import { MODES } from '../constants'; import type { AdapterWriteFailure, ApplyExpandedThemeOverride, ConcreteTokenValue, ModeTokenState, ToUniwindMaps, TokenPath, } from '../override/contracts'; import type { Mode, Platform } from '../types'; export type UniwindAdapterContractErrorReason = | 'missing-runtime-mapping' | 'conflicting-runtime-mapping' | 'inconsistent-written-keys'; export class UniwindAdapterContractError extends Error { readonly reason: UniwindAdapterContractErrorReason; readonly mode: Mode; readonly tokenPath?: TokenPath; constructor( reason: UniwindAdapterContractErrorReason, mode: Mode, message: string, tokenPath?: TokenPath, ) { super(message); this.name = 'UniwindAdapterContractError'; this.reason = reason; this.mode = mode; this.tokenPath = tokenPath; } } export interface MapTokenWritesToUniwindInput { readonly platform: Platform; readonly writes: ModeTokenState; readonly runtimeMap: Readonly>; } function readRuntimeMapping( runtimeMap: Readonly>, tokenPath: TokenPath, ): string | undefined { return Object.hasOwn(runtimeMap, tokenPath) ? runtimeMap[tokenPath] : undefined; } export function resolveRuntimeCssVariable( runtimeMap: Readonly>, platform: Platform, mode: Mode, tokenPath: TokenPath, ): string | undefined { return ( readRuntimeMapping(runtimeMap, `modes.${mode}.${tokenPath}`) ?? readRuntimeMapping(runtimeMap, `platform.${platform}.${tokenPath}`) ?? readRuntimeMapping(runtimeMap, tokenPath) ); } function mapModeWrites( mode: Mode, platform: Platform, writes: Readonly>, runtimeMap: Readonly>, ): Readonly> { const variables: Record = {}; for (const [tokenPath, value] of Object.entries(writes)) { const cssVariable = resolveRuntimeCssVariable(runtimeMap, platform, mode, tokenPath); if (!cssVariable) { throw new UniwindAdapterContractError( 'missing-runtime-mapping', mode, `No runtime CSS variable maps target token "${tokenPath}" for ${platform}/${mode}.`, tokenPath, ); } const stringValue = String(value); if (Object.hasOwn(variables, cssVariable) && variables[cssVariable] !== stringValue) { throw new UniwindAdapterContractError( 'conflicting-runtime-mapping', mode, `Target tokens mapped conflicting values to runtime variable "${cssVariable}" in ${mode}.`, tokenPath, ); } variables[cssVariable] = stringValue; } return variables; } export function mapTokenWritesToUniwind({ platform, writes, runtimeMap, }: MapTokenWritesToUniwindInput): ModeTokenState { return { light: mapModeWrites('light', platform, writes.light, runtimeMap), dark: mapModeWrites('dark', platform, writes.dark, runtimeMap), }; } function assertWrittenKeysMatch( mode: Mode, writes: Readonly>, writtenKeys: readonly TokenPath[], ): void { const writeKeys = Object.keys(writes); const declaredKeys = new Set(writtenKeys); if ( declaredKeys.size !== writtenKeys.length || writeKeys.length !== declaredKeys.size || writeKeys.some((key) => !declaredKeys.has(key)) ) { throw new UniwindAdapterContractError( 'inconsistent-written-keys', mode, `Expanded ${mode} writes and writtenKeys must contain the same unique token paths.`, ); } } export const toUniwindMaps: ToUniwindMaps = ({ expanded, runtimeMap }) => { for (const mode of MODES) { assertWrittenKeysMatch(mode, expanded.writes[mode], expanded.writtenKeys[mode]); } return mapTokenWritesToUniwind({ platform: expanded.platform, writes: expanded.writes, runtimeMap, }); }; function adapterFailure(mode: Mode, artifactRevision: string, cause: unknown): AdapterWriteFailure { return { code: 'override.adapter_write_failed', mode, artifactRevision, cause, recovery: 'reload-and-reapply', }; } export const applyExpandedThemeOverride: ApplyExpandedThemeOverride = ({ expanded, runtimeMap, writer, }) => { let maps: ModeTokenState; try { maps = toUniwindMaps({ expanded, runtimeMap }); } catch (cause) { const mode = cause instanceof UniwindAdapterContractError ? cause.mode : 'light'; return { applied: false, failure: adapterFailure(mode, expanded.revision.id, cause), }; } for (const mode of MODES) { if (Object.keys(maps[mode]).length === 0) continue; try { writer(mode, maps[mode]); } catch (cause) { return { applied: false, failure: adapterFailure(mode, expanded.revision.id, cause), }; } } return { applied: true, revision: expanded.revision.id, }; };