import type { DesignSystem } from '../../design-system'
import type { PluginWithConfig } from '../../plugin-api'
import { createThemeFn } from '../plugin-functions'
import { deepMerge, isPlainObject } from './deep-merge'
import {
type ResolvedConfig,
type ResolvedContentConfig,
type ResolvedThemeValue,
type ThemeValue,
type UserConfig,
} from './types'
export interface ConfigFile {
path?: string
config: UserConfig
}
interface ResolutionContext {
design: DesignSystem
configs: UserConfig[]
plugins: PluginWithConfig[]
content: ResolvedContentConfig
theme: Record<string, ThemeValue>
extend: Record<string, ThemeValue[]>
result: ResolvedConfig
}
let minimal: ResolvedConfig = {
darkMode: null,
theme: {},
plugins: [],
content: {
files: [],
},
}
export function resolveConfig(design: DesignSystem, files: ConfigFile[]): ResolvedConfig {
let ctx: ResolutionContext = {
design,
configs: [],
plugins: [],
content: {
files: [],
},
theme: {},
extend: {},
result: structuredClone(minimal),
}
for (let file of files) {
extractConfigs(ctx, file)
}
for (let config of ctx.configs) {
if ('darkMode' in config && config.darkMode !== undefined) {
ctx.result.darkMode = config.darkMode ?? null
}
}
mergeTheme(ctx)
return {
...ctx.result,
content: ctx.content,
theme: ctx.theme as ResolvedConfig['theme'],
plugins: ctx.plugins,
}
}
function mergeThemeExtension(
themeValue: ThemeValue | ThemeValue[],
extensionValue: ThemeValue | ThemeValue[],
) {
if (Array.isArray(themeValue) && isPlainObject(themeValue[0])) {
return themeValue.concat(extensionValue)
}
if (
Array.isArray(extensionValue) &&
isPlainObject(extensionValue[0]) &&
isPlainObject(themeValue)
) {
return [themeValue, ...extensionValue]
}
if (Array.isArray(extensionValue)) {
return extensionValue
}
return undefined
}
export interface PluginUtils {
theme(keypath: string, defaultValue?: any): any
}
function extractConfigs(ctx: ResolutionContext, { config, path }: ConfigFile): void {
let plugins: PluginWithConfig[] = []
for (let plugin of config.plugins ?? []) {
if ('__isOptionsFunction' in plugin) {
plugins.push(plugin())
} else if ('handler' in plugin) {
plugins.push(plugin)
} else {
plugins.push({ handler: plugin })
}
}
if (Array.isArray(config.presets) && config.presets.length === 0) {
throw new Error(
'Error in the config file/plugin/preset. An empty preset (`preset: []`) is not currently supported.',
)
}
for (let preset of config.presets ?? []) {
extractConfigs(ctx, { path, config: preset })
}
for (let plugin of plugins) {
ctx.plugins.push(plugin)
if (plugin.config) {
extractConfigs(ctx, { path, config: plugin.config })
}
}
let content = config.content ?? []
let files = Array.isArray(content) ? content : content.files
for (let file of files) {
ctx.content.files.push(typeof file === 'object' ? file : { base: path!, pattern: file })
}
ctx.configs.push(config)
}
function mergeTheme(ctx: ResolutionContext) {
let api: PluginUtils = {
theme: createThemeFn(ctx.design, () => ctx.theme, resolveValue),
}
function resolveValue(value: ThemeValue | null | undefined): ResolvedThemeValue {
if (typeof value === 'function') {
return value(api) ?? null
}
return value ?? null
}
for (let config of ctx.configs) {
let theme = config.theme ?? {}
let extend = theme.extend ?? {}
Object.assign(ctx.theme, theme)
for (let key in extend) {
ctx.extend[key] ??= []
ctx.extend[key].push(extend[key])
}
}
delete ctx.theme.extend
for (let key in ctx.extend) {
let values = [ctx.theme[key], ...ctx.extend[key]]
ctx.theme[key] = () => {
let v = values.map(resolveValue)
let result = deepMerge({}, v, mergeThemeExtension)
return result
}
}
for (let key in ctx.theme) {
ctx.theme[key] = resolveValue(ctx.theme[key])
}
}