Files
tutti/tools/scripts/check-ui-renderer-tokens.mjs
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

153 lines
4.2 KiB
JavaScript

import { readFile, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const scriptDirectory = dirname(fileURLToPath(import.meta.url));
const workspaceRoot = join(scriptDirectory, "..", "..");
const sourcePath = join(
workspaceRoot,
"packages/ui/system/src/tokens/renderer-theme.json"
);
const cssOutputPath = join(
workspaceRoot,
"packages/ui/system/src/styles/generated/renderer-theme.css"
);
const nativeOutputPath = join(
workspaceRoot,
"packages/ui/system/src/native/generated-tokens.ts"
);
const source = JSON.parse(await readFile(sourcePath, "utf8"));
validateSource(source);
const expectedOutputs = new Map([
[cssOutputPath, renderCss(source)],
[nativeOutputPath, renderNativeTokens(source)]
]);
const writeOutputs = process.argv.includes("--write");
if (writeOutputs) {
await Promise.all(
Array.from(expectedOutputs, ([path, output]) => writeFile(path, output))
);
}
const stalePaths = [];
for (const [path, expected] of expectedOutputs) {
const actual = await readFile(path, "utf8");
if (actual !== expected) {
stalePaths.push(path);
}
}
if (stalePaths.length > 0) {
console.error(
"Renderer token outputs are stale. Update them from packages/ui/system/src/tokens/renderer-theme.json:"
);
for (const path of stalePaths) {
console.error(`- ${path}`);
}
process.exitCode = 1;
} else {
console.log(
writeOutputs
? "renderer token outputs generated"
: "renderer token check passed"
);
}
function validateSource(value) {
if (value.schemaVersion !== 1) {
throw new Error("renderer theme manifest must use schemaVersion 1");
}
for (const key of ["control", "radius", "space"]) {
if (typeof value.dimensions?.[key] !== "object") {
throw new Error(`renderer theme manifest is missing dimensions.${key}`);
}
}
const lightKeys = Object.keys(value.themes?.light ?? {});
const darkKeys = Object.keys(value.themes?.dark ?? {});
if (lightKeys.length === 0 || lightKeys.join("|") !== darkKeys.join("|")) {
throw new Error("light and dark renderer token keys must match");
}
for (const mode of ["light", "dark"]) {
for (const key of lightKeys) {
const token = value.themes[mode][key];
if (typeof token?.native !== "string" || typeof token.web !== "string") {
throw new Error(
`renderer token ${mode}.${key} must define string native and web values`
);
}
}
}
}
function renderCss(value) {
const light = renderCssThemeBlock(":root", value.themes.light);
const dark = renderCssThemeBlock(
':root[data-theme="dark"]',
value.themes.dark
);
const automaticDark = renderCssThemeBlock(
':root:not([data-theme="light"])',
value.themes.dark,
" "
);
return [
"/* This file is generated from ../tokens/renderer-theme.json. Do not edit it directly. */",
light,
"",
dark,
"",
"@media (prefers-color-scheme: dark) {",
automaticDark,
"}",
""
].join("\n");
}
function renderCssThemeBlock(selector, theme, indent = "") {
const declarations = Object.entries(theme).map(
([key, token]) => `${indent} --renderer-${toKebabCase(key)}: ${token.web};`
);
return [`${indent}${selector} {`, ...declarations, `${indent}}`].join("\n");
}
function renderNativeTokens(value) {
const nativeThemes = Object.fromEntries(
Object.entries(value.themes).map(([mode, theme]) => [
mode,
Object.fromEntries(
Object.entries(theme).map(([key, token]) => [key, token.native])
)
])
);
return [
"/* This file is generated from ../tokens/renderer-theme.json. Do not edit it directly. */",
`export const nativeThemeDimensions = ${formatTypescriptObject(value.dimensions)} as const;`,
"",
`export const nativeThemes = ${formatTypescriptObject(nativeThemes)} as const;`,
"",
"export type NativeThemeMode = keyof typeof nativeThemes;",
"export type NativeThemePalette = (typeof nativeThemes)[NativeThemeMode];",
""
].join("\n");
}
function formatTypescriptObject(value) {
return JSON.stringify(value, null, 2).replace(
/^(\s*)"([A-Za-z_$][A-Za-z0-9_$]*)":/gm,
"$1$2:"
);
}
function toKebabCase(value) {
return value.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
}