Files
WRNexusJS/packages/design-system/src/compile.ts
T

93 lines
3.7 KiB
TypeScript

import type { DesignSystem, LegacyTheme, ResolveOptions, ResolvedDesignSystem } from "./types.ts";
import { resolveDesignSystem, fail } from "./resolve.ts";
import { validName } from "./validation.ts";
import { compileComponentRecipes } from "./recipes.ts";
function declarations(system: ResolvedDesignSystem, legacyOnly = false): Record<string, string> {
const result: Record<string, string> = {};
const owners = new Map<string, string>();
for (const [name, token] of Object.entries(system.tokens)) {
const keys = legacyOnly
? token.legacy
? [token.legacy]
: []
: [
`--wrn-ds-${name.replace(/\./g, "-")}`,
...(token.legacy
? [token.legacy === "color-scheme" ? "color-scheme" : `--wrn-${token.legacy}`]
: []),
];
for (const key of keys) {
const previous = owners.get(key);
if (previous && previous !== name)
fail("WRN-DS-COLLISION", name, `Output '${key}' is already bound to '${previous}'.`);
owners.set(key, name);
result[key] = String(token.value);
}
}
return Object.fromEntries(
Object.entries(result).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)),
);
}
/** Compile deterministic tokens and versioned core-component recipes. */
export function compileDesignSystem(
system: DesignSystem,
options: ResolveOptions & { scope?: string } = {},
): { css: string; recipeCss: string; resolved: ResolvedDesignSystem } {
if (options.scope !== undefined && !validName(options.scope))
fail("WRN-DS-NAME", "scope", "Invalid scope identifier.");
const resolved = resolveDesignSystem(system, options);
const selector = options.scope ? `[data-wrn-design="${options.scope}"]` : ":root";
const block = (target: string, value: ResolvedDesignSystem) =>
`${target}{${Object.entries(declarations(value))
.map(([key, val]) => `${key}:${val};`)
.join("")}}`;
const blocks = [block(selector, resolved)];
const defaultRecipes = compileComponentRecipes(resolved, selector, options.scope);
const recipeBlocks = [defaultRecipes];
for (const mode of resolved.modes) {
blocks.push(
block(`${selector}[data-theme="${mode}"]`, resolveDesignSystem(system, { ...options, mode })),
);
const modeResolved = resolveDesignSystem(system, { ...options, mode });
// Legacy-bound references already follow mode variables. Avoid repeating identical rules.
const normalized = compileComponentRecipes(modeResolved, selector, options.scope);
if (normalized !== defaultRecipes)
recipeBlocks.push(
compileComponentRecipes(modeResolved, `${selector}[data-theme="${mode}"]`, options.scope),
);
}
const recipeCss = recipeBlocks.filter(Boolean).join("\n");
return {
css: blocks.join("\n") + "\n" + (recipeCss ? recipeCss + "\n" : ""),
recipeCss,
resolved,
};
}
/** Return existing ThemeConfig-compatible data, retaining explicit app overrides. */
export function adaptLegacyTheme<T extends LegacyTheme>(
system: DesignSystem,
legacy: T,
options: ResolveOptions = {},
): T & LegacyTheme {
const resolved = resolveDesignSystem(system, options);
const base = declarations(resolved, true);
const names = [
...new Set(["light", "dark", ...resolved.modes, ...Object.keys(legacy.themes ?? {})]),
].sort();
const themes: Record<string, Record<string, string>> = {};
for (const name of names) {
const mapped = resolved.modes.includes(name)
? declarations(resolveDesignSystem(system, { ...options, mode: name }), true)
: base;
themes[name] = { ...mapped, ...(legacy.themes?.[name] ?? {}) };
}
return {
...legacy,
...(legacy.default ? {} : resolved.mode ? { default: resolved.mode } : {}),
themes,
};
}