import type { DesignSystem } from '../../design-system'
import type { SourceLocation } from '../../source-maps/source'
import colors from '../colors'
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
base: string
config: UserConfig
reference: boolean
src: SourceLocation | undefined
}
interface ResolutionContext {
design: DesignSystem
configs: UserConfig[]
plugins: PluginWithConfig[]
content: ResolvedContentConfig
theme: Record<string, ThemeValue>
extend: Record<string, ThemeValue[]>
result: ResolvedConfig
}
let minimal: ResolvedConfig = {
blocklist: [],
future: {},
experimental: {},
prefix: '',
important: false,
darkMode: null,
theme: {},
plugins: [],
content: {
files: [],
},
}
export function resolveConfig(
design: DesignSystem,
files: ConfigFile[],
): { resolvedConfig: ResolvedConfig; replacedThemeKeys: Set<string> } {
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
}
if ('prefix' in config && config.prefix !== undefined) {
ctx.result.prefix = config.prefix ?? ''
}
if ('blocklist' in config && config.blocklist !== undefined) {
ctx.result.blocklist = config.blocklist ?? []
}
if ('important' in config && config.important !== undefined) {
ctx.result.important = config.important ?? false
}
}
let replacedThemeKeys = mergeTheme(ctx)
return {
resolvedConfig: {
...ctx.result,
content: ctx.content,
theme: ctx.theme as ResolvedConfig['theme'],
plugins: ctx.plugins,
},
replacedThemeKeys,
}
}
export 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 type PluginUtils = {
theme: (keypath: string, defaultValue?: any) => any
colors: typeof colors
}
function extractConfigs(
ctx: ResolutionContext,
{ config, base, path, reference, src }: ConfigFile,
): void {
let plugins: PluginWithConfig[] = []
for (let plugin of config.plugins ?? []) {
if ('__isOptionsFunction' in plugin) {
plugins.push({ ...plugin(), reference, src })
} else if ('handler' in plugin) {
plugins.push({ ...plugin, reference, src })
} else {
plugins.push({ handler: plugin, reference, src })
}
}
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, base, config: preset, reference, src })
}
for (let plugin of plugins) {
ctx.plugins.push(plugin)
if (plugin.config) {
extractConfigs(ctx, {
path,
base,
config: plugin.config,
reference: !!plugin.reference,
src: plugin.src ?? src,
})
}
}
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, pattern: file })
}
ctx.configs.push(config)
}
function mergeTheme(ctx: ResolutionContext): Set<string> {
let replacedThemeKeys: Set<string> = new Set()
let themeFn = createThemeFn(ctx.design, () => ctx.theme, resolveValue)
let theme = Object.assign(themeFn, {
theme: themeFn,
colors,
})
function resolveValue(value: ThemeValue | null | undefined): ResolvedThemeValue {
if (typeof value === 'function') {
return value(theme) ?? null
}
return value ?? null
}
for (let config of ctx.configs) {
let theme = config.theme ?? {}
let extend = theme.extend ?? {}
for (let key in theme) {
if (key === 'extend') {
continue
}
replacedThemeKeys.add(key)
}
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])
}
if (ctx.theme.screens && typeof ctx.theme.screens === 'object') {
for (let key of Object.keys(ctx.theme.screens)) {
let screen = ctx.theme.screens[key]
if (!screen) continue
if (typeof screen !== 'object') continue
if ('raw' in screen) continue
if ('max' in screen) continue
if (!('min' in screen)) continue
ctx.theme.screens[key] = screen.min
}
}
return replacedThemeKeys
}