feat: release design system engine, studio and component contracts
This commit is contained in:
1 parent
ff1065fb27
commit
3943b946d6
693 files changed
+175546
-3894
No files matched your search
@@ -0,0 +1,191 @@
|
||||
import {
|
||||
resolveDesignSystem,
|
||||
validateDesignSystem,
|
||||
type DesignSystem,
|
||||
type DesignDiagnostic,
|
||||
} from "@wrnexus/design-system";
|
||||
|
||||
export type DecisionLock = string;
|
||||
const forbidden = new Set(["__proto__", "prototype", "constructor"]);
|
||||
export function decisionPath(path: string): string[] {
|
||||
if (typeof path !== "string" || path.length > 512 || !path.startsWith("/"))
|
||||
throw Error("WRN-STUDIO-LOCK-PATH: use a JSON pointer.");
|
||||
const parts = path
|
||||
.slice(1)
|
||||
.split("/")
|
||||
.map((p) => {
|
||||
if (/~(?![01])/u.test(p)) throw Error("Invalid pointer escape.");
|
||||
return p.replace(/~1/g, "/").replace(/~0/g, "~");
|
||||
});
|
||||
if (
|
||||
!["tokens", "modes", "densities", "recipes", "name", "id", "version", "defaultMode"].includes(
|
||||
parts[0]!,
|
||||
) ||
|
||||
parts.some((p) => !p || forbidden.has(p)) ||
|
||||
parts.length > 8
|
||||
)
|
||||
throw Error("WRN-STUDIO-LOCK-PATH: unsupported decision path.");
|
||||
return parts;
|
||||
}
|
||||
export const pointerPart = (value: string) => value.replace(/~/g, "~0").replace(/\//g, "~1");
|
||||
export function readDecision(value: unknown, path: string): unknown {
|
||||
let current = value;
|
||||
for (const part of decisionPath(path)) {
|
||||
if (!current || typeof current !== "object" || !Object.hasOwn(current, part)) return undefined;
|
||||
current = (current as Record<string, unknown>)[part];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
export function writeDecision(system: DesignSystem, path: string, value: unknown) {
|
||||
const parts = decisionPath(path);
|
||||
let target = system as unknown as Record<string, unknown>;
|
||||
for (const part of parts.slice(0, -1)) {
|
||||
if (!Object.hasOwn(target, part)) target[part] = {};
|
||||
if (!target[part] || typeof target[part] !== "object" || Array.isArray(target[part]))
|
||||
throw Error("WRN-STUDIO-PATCH: parent must be an object.");
|
||||
target = target[part] as Record<string, unknown>;
|
||||
}
|
||||
const last = parts.at(-1)!;
|
||||
if (value === undefined) delete target[last];
|
||||
else target[last] = structuredClone(value);
|
||||
}
|
||||
export function validateLocks(locks: unknown): asserts locks is DecisionLock[] {
|
||||
if (
|
||||
!Array.isArray(locks) ||
|
||||
locks.length > 128 ||
|
||||
locks.some((p) => typeof p !== "string") ||
|
||||
new Set(locks).size !== locks.length
|
||||
)
|
||||
throw Error("WRN-STUDIO-LOCKS: provide up to 128 unique decision paths.");
|
||||
for (const path of locks) decisionPath(path);
|
||||
}
|
||||
function references(value: unknown, result = new Set<string>()) {
|
||||
if (value && typeof value === "object") {
|
||||
if ("ref" in value && typeof value.ref === "string") result.add(value.ref);
|
||||
else for (const child of Object.values(value)) references(child, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function tokenNames(system: DesignSystem, path: string) {
|
||||
const [kind, layer, name] = decisionPath(path);
|
||||
const names = new Set<string>();
|
||||
if (kind === "tokens")
|
||||
for (const token of Object.keys(system.tokens)) if (!layer || token === layer) names.add(token);
|
||||
if (kind === "modes" || kind === "densities") {
|
||||
const layers = system[kind] ?? {};
|
||||
for (const [key, tokens] of Object.entries(layers))
|
||||
if (!layer || key === layer)
|
||||
for (const token of Object.keys(tokens)) if (!name || name === token) names.add(token);
|
||||
}
|
||||
if (kind === "recipes") references(readDecision(system, path), names);
|
||||
return names;
|
||||
}
|
||||
function configurations(a: DesignSystem, b: DesignSystem) {
|
||||
const modes = [
|
||||
undefined,
|
||||
...new Set([...Object.keys(a.modes ?? {}), ...Object.keys(b.modes ?? {})]),
|
||||
];
|
||||
const densities = [
|
||||
undefined,
|
||||
...new Set([...Object.keys(a.densities ?? {}), ...Object.keys(b.densities ?? {})]),
|
||||
];
|
||||
if (modes.length * densities.length > 128)
|
||||
throw Error("WRN-STUDIO-LOCK-LIMIT: too many lock configurations.");
|
||||
return modes.flatMap((mode) => densities.map((density) => ({ mode, density })));
|
||||
}
|
||||
/** Locks protect raw decisions and effective referenced values, including aliases and modes. */
|
||||
export function checkDecisionLocks(
|
||||
before: DesignSystem,
|
||||
after: DesignSystem,
|
||||
locks: DecisionLock[],
|
||||
): DesignDiagnostic[] {
|
||||
validateLocks(locks);
|
||||
if (!locks.length) return [];
|
||||
const diagnostics: DesignDiagnostic[] = [];
|
||||
const fail = (path: string, message: string) =>
|
||||
diagnostics.push({ code: "WRN-STUDIO-LOCK", path, message });
|
||||
const configurationsList = configurations(before, after);
|
||||
const resolve = (system: DesignSystem, mode?: string, density?: string) =>
|
||||
resolveDesignSystem(system, {
|
||||
mode: mode && Object.hasOwn(system.modes ?? {}, mode) ? mode : undefined,
|
||||
density: density && Object.hasOwn(system.densities ?? {}, density) ? density : undefined,
|
||||
}).tokens;
|
||||
const pairs = configurationsList.map(({ mode, density }) => ({
|
||||
mode,
|
||||
density,
|
||||
before: resolve(before, mode, density),
|
||||
after: resolve(after, mode, density),
|
||||
}));
|
||||
for (const path of locks) {
|
||||
if (JSON.stringify(readDecision(before, path)) !== JSON.stringify(readDecision(after, path))) {
|
||||
fail(path, "This decision is locked. Unlock it before changing or removing it.");
|
||||
continue;
|
||||
}
|
||||
const names = tokenNames(before, path);
|
||||
let changed = false;
|
||||
for (const pair of pairs) {
|
||||
const parts = decisionPath(path);
|
||||
if (parts[0] === "modes" && parts[1] && pair.mode !== parts[1]) continue;
|
||||
if (parts[0] === "densities" && parts[1] && pair.density !== parts[1]) continue;
|
||||
for (const name of names)
|
||||
if (
|
||||
JSON.stringify({ type: pair.before[name]?.type, value: pair.before[name]?.value }) !==
|
||||
JSON.stringify({ type: pair.after[name]?.type, value: pair.after[name]?.value })
|
||||
) {
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
if (changed) break;
|
||||
}
|
||||
if (changed)
|
||||
fail(path, "A referenced token or mode changes the effective value of this locked decision.");
|
||||
}
|
||||
return diagnostics;
|
||||
}
|
||||
/** Local directions copy lock dependencies; arbitrary provider patches must pass checks unchanged. */
|
||||
export function preserveDecisionLocks(
|
||||
before: DesignSystem,
|
||||
after: DesignSystem,
|
||||
locks: DecisionLock[],
|
||||
) {
|
||||
validateLocks(locks);
|
||||
const next = structuredClone(after);
|
||||
const names = new Set<string>();
|
||||
for (const path of locks) {
|
||||
writeDecision(next, path, readDecision(before, path));
|
||||
for (const name of tokenNames(before, path)) names.add(name);
|
||||
}
|
||||
// The default selection is part of an effective token/recipe decision.
|
||||
if (names.size) next.defaultMode = before.defaultMode;
|
||||
for (const name of names) {
|
||||
for (const token of [
|
||||
before.tokens[name],
|
||||
...Object.values(before.modes ?? {}).map((t) => t[name]),
|
||||
...Object.values(before.densities ?? {}).map((t) => t[name]),
|
||||
])
|
||||
for (const ref of references(token)) names.add(ref);
|
||||
writeDecision(next, "/tokens/" + pointerPart(name), before.tokens[name]);
|
||||
for (const kind of ["modes", "densities"] as const)
|
||||
for (const layer of new Set([
|
||||
...Object.keys(before[kind] ?? {}),
|
||||
...Object.keys(next[kind] ?? {}),
|
||||
]))
|
||||
writeDecision(
|
||||
next,
|
||||
`/${kind}/${pointerPart(layer)}/${pointerPart(name)}`,
|
||||
before[kind]?.[layer]?.[name],
|
||||
);
|
||||
}
|
||||
if (validateDesignSystem(next).length)
|
||||
throw Error("WRN-STUDIO-LOCK: locked decisions cannot be combined with this direction.");
|
||||
const failures = checkDecisionLocks(before, next, locks);
|
||||
if (failures.length) throw Error(failures.map((d) => d.message).join(" "));
|
||||
return next;
|
||||
}
|
||||
export async function designRevision(system: DesignSystem, locks: DecisionLock[] = []) {
|
||||
validateLocks(locks);
|
||||
const bytes = new TextEncoder().encode(JSON.stringify({ system, locks: [...locks].sort() }));
|
||||
return [...new Uint8Array(await crypto.subtle.digest("SHA-256", bytes))]
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
}
|
||||
Reference in new issue
Block a user