mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
Ralph iteration 1: work in progress
This commit is contained in:
@@ -421,12 +421,225 @@ async function checkToolsLayout(): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
const stylePolicySkippedDirectories = new Set([
|
||||
".next",
|
||||
".od-data",
|
||||
"dist",
|
||||
"node_modules",
|
||||
"out",
|
||||
"reports",
|
||||
"test-results",
|
||||
]);
|
||||
|
||||
const stylePolicySourcePrefixes = ["apps/web/app/", "apps/web/src/"];
|
||||
const stylePolicyHardcodedColorEnforcedPrefixes = ["scripts/guard-style-policy-fixtures/"];
|
||||
const stylePolicyExtensions = new Set([".css", ".ts", ".tsx"]);
|
||||
const tailwindDefaultColorNames = [
|
||||
"slate",
|
||||
"gray",
|
||||
"zinc",
|
||||
"neutral",
|
||||
"stone",
|
||||
"red",
|
||||
"orange",
|
||||
"amber",
|
||||
"yellow",
|
||||
"lime",
|
||||
"green",
|
||||
"emerald",
|
||||
"teal",
|
||||
"cyan",
|
||||
"sky",
|
||||
"blue",
|
||||
"indigo",
|
||||
"violet",
|
||||
"purple",
|
||||
"fuchsia",
|
||||
"pink",
|
||||
"rose",
|
||||
"white",
|
||||
"black",
|
||||
].join("|");
|
||||
const defaultTailwindPaletteClassPattern = new RegExp(
|
||||
`\\b(?:bg|text|border|from|via|to|ring|outline|decoration|accent|caret|fill|stroke)-(?:${tailwindDefaultColorNames})(?:-\\d{2,3})?\\b`,
|
||||
"g",
|
||||
);
|
||||
|
||||
const cssWideAndSpecialColorKeywords = new Set(["transparent", "currentcolor", "inherit", "initial", "unset", "revert"]);
|
||||
const realNamedColors = [
|
||||
"black",
|
||||
"blue",
|
||||
"brown",
|
||||
"cyan",
|
||||
"gold",
|
||||
"gray",
|
||||
"green",
|
||||
"grey",
|
||||
"lime",
|
||||
"magenta",
|
||||
"maroon",
|
||||
"navy",
|
||||
"orange",
|
||||
"pink",
|
||||
"purple",
|
||||
"red",
|
||||
"silver",
|
||||
"teal",
|
||||
"violet",
|
||||
"white",
|
||||
"yellow",
|
||||
].join("|");
|
||||
const hardcodedColorPattern = new RegExp(
|
||||
`#[0-9a-fA-F]{3,8}\\b|rgba?\\([^)]*\\)|hsla?\\([^)]*\\)|(?<quote>['\"])\\s*(?<named>${realNamedColors}|transparent|currentColor|currentcolor|inherit|initial|unset|revert)\\s*\\k<quote>`,
|
||||
"g",
|
||||
);
|
||||
|
||||
type StylePolicyAllowlistEntry = {
|
||||
pathPattern: RegExp;
|
||||
valuePattern: RegExp;
|
||||
reason: string;
|
||||
};
|
||||
|
||||
const hardcodedColorAllowlist: StylePolicyAllowlistEntry[] = [
|
||||
{
|
||||
pathPattern: /^apps\/web\/src\/index\.css$/,
|
||||
valuePattern: /^(?:#[0-9a-fA-F]{3,8}\b|rgba?\([^)]*\)|hsla?\([^)]*\))$/,
|
||||
reason: "global token definitions, shadows, overlays, and retained migration inventory live in the CSS source of truth",
|
||||
},
|
||||
{
|
||||
pathPattern: /^apps\/web\/src\/components\/(?:AgentIcon|PaletteTweaks|PetSettings|SettingsDialog)\.tsx$/,
|
||||
valuePattern: /^(?:#[0-9a-fA-F]{3,8}\b|rgba?\([^)]*\)|hsla?\([^)]*\))$/,
|
||||
reason: "brand accents, user accent choices, and legacy token fallbacks are classified as Phase 1 migration inventory",
|
||||
},
|
||||
{
|
||||
pathPattern: /^apps\/web\/src\/components\/(?:SketchEditor|SketchPreview|NewProjectPanel)\.tsx$/,
|
||||
valuePattern: /^(?:#[0-9a-fA-F]{3,8}\b|rgba?\([^)]*\)|hsla?\([^)]*\)|['\"](?:none|currentColor|currentcolor|transparent)['\"])$/,
|
||||
reason: "sketch/canvas data and SVG illustrations keep narrow hardcoded color exceptions until their migration slice",
|
||||
},
|
||||
{
|
||||
pathPattern: /^apps\/web\/src\/components\/(?:FileViewer|ManualEditPanel)\.tsx$/,
|
||||
valuePattern: /^(?:#[0-9a-fA-F]{3,8}\b|rgba?\([^)]*\)|hsla?\([^)]*\))$/,
|
||||
reason: "user-authored file, inspect, and editable style colors are handled by the file/viewer migration slice",
|
||||
},
|
||||
{
|
||||
pathPattern: /^apps\/web\/src\/components\/(?:MemorySection|MemoryModelInline|MemoryToast)\.tsx$/,
|
||||
valuePattern: /^(?:#[0-9a-fA-F]{3,8}\b|rgba?\([^)]*\)|hsla?\([^)]*\))$/,
|
||||
reason: "memory UI legacy color fallbacks are classified as Phase 1 migration inventory",
|
||||
},
|
||||
{
|
||||
pathPattern: /^apps\/web\/tests\//,
|
||||
valuePattern: /.*/,
|
||||
reason: "tests and fixtures may assert rejected colors explicitly",
|
||||
},
|
||||
];
|
||||
|
||||
type StylePolicyViolation = {
|
||||
filePath: string;
|
||||
lineNumber: number;
|
||||
match: string;
|
||||
reason: string;
|
||||
};
|
||||
|
||||
function lineNumberForIndex(source: string, index: number): number {
|
||||
return source.slice(0, index).split("\n").length;
|
||||
}
|
||||
|
||||
function isStylePolicySource(repositoryPath: string): boolean {
|
||||
return stylePolicySourcePrefixes.some((prefix) => repositoryPath.startsWith(prefix));
|
||||
}
|
||||
|
||||
function isHardcodedColorEnforcedPath(repositoryPath: string): boolean {
|
||||
return stylePolicyHardcodedColorEnforcedPrefixes.some((prefix) => repositoryPath.startsWith(prefix));
|
||||
}
|
||||
|
||||
function isHardcodedColorAllowlisted(repositoryPath: string, match: string): boolean {
|
||||
const normalizedMatch = match.trim();
|
||||
const unquotedMatch = normalizedMatch.replace(/^['"]|['"]$/g, "");
|
||||
if (cssWideAndSpecialColorKeywords.has(unquotedMatch.toLowerCase())) return true;
|
||||
|
||||
return hardcodedColorAllowlist.some(
|
||||
(entry) => entry.pathPattern.test(repositoryPath) && entry.valuePattern.test(normalizedMatch),
|
||||
);
|
||||
}
|
||||
|
||||
function collectStylePolicyViolationsFromSource(repositoryPath: string, source: string): StylePolicyViolation[] {
|
||||
const violations: StylePolicyViolation[] = [];
|
||||
|
||||
if (isStylePolicySource(repositoryPath)) {
|
||||
for (const match of source.matchAll(defaultTailwindPaletteClassPattern)) {
|
||||
violations.push({
|
||||
filePath: repositoryPath,
|
||||
lineNumber: lineNumberForIndex(source, match.index ?? 0),
|
||||
match: match[0],
|
||||
reason: "default Tailwind palette classes must use Open Design token utilities instead",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (isStylePolicySource(repositoryPath) || isHardcodedColorEnforcedPath(repositoryPath)) {
|
||||
for (const match of source.matchAll(hardcodedColorPattern)) {
|
||||
const value = match[0];
|
||||
if (isHardcodedColorAllowlisted(repositoryPath, value)) continue;
|
||||
if (!isHardcodedColorEnforcedPath(repositoryPath)) continue;
|
||||
|
||||
violations.push({
|
||||
filePath: repositoryPath,
|
||||
lineNumber: lineNumberForIndex(source, match.index ?? 0),
|
||||
match: value,
|
||||
reason: "unregistered hardcoded UI colors must use Open Design tokens or an explicit allowlist entry",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return violations;
|
||||
}
|
||||
|
||||
async function collectStylePolicyViolations(directory: string): Promise<StylePolicyViolation[]> {
|
||||
const entries = await readdir(directory, { withFileTypes: true });
|
||||
const violations: StylePolicyViolation[] = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(directory, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
if (stylePolicySkippedDirectories.has(entry.name)) continue;
|
||||
violations.push(...(await collectStylePolicyViolations(fullPath)));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!entry.isFile() || !stylePolicyExtensions.has(path.extname(entry.name))) continue;
|
||||
|
||||
const repositoryPath = toRepositoryPath(fullPath);
|
||||
if (!isStylePolicySource(repositoryPath) && !isHardcodedColorEnforcedPath(repositoryPath)) continue;
|
||||
|
||||
violations.push(...collectStylePolicyViolationsFromSource(repositoryPath, await readFile(fullPath, "utf8")));
|
||||
}
|
||||
|
||||
return violations;
|
||||
}
|
||||
|
||||
async function checkStylePolicy(): Promise<boolean> {
|
||||
const violations = await collectStylePolicyViolations(repoRoot);
|
||||
|
||||
if (violations.length > 0) {
|
||||
console.error("Style policy violations found:");
|
||||
for (const violation of violations) {
|
||||
console.error(`- ${violation.filePath}:${violation.lineNumber} \`${violation.match}\` -> ${violation.reason}`);
|
||||
}
|
||||
console.error("Use Open Design token utilities/CSS variables or add a narrow allowlist entry with a reason.");
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log("Style policy check passed: Tailwind palette classes and enforced hardcoded UI colors stay token-first.");
|
||||
return true;
|
||||
}
|
||||
|
||||
const checks: GuardCheck[] = [
|
||||
{ name: "residual JavaScript", run: checkResidualJavaScript },
|
||||
{ name: "test layout", run: checkTestLayout },
|
||||
{ name: "e2e layout", run: checkE2eLayout },
|
||||
{ name: "web test layout", run: checkWebTestLayout },
|
||||
{ name: "tools layout", run: checkToolsLayout },
|
||||
{ name: "style policy", run: checkStylePolicy },
|
||||
{ name: "design system token-fixture sync", run: checkDesignSystemTokenFixtureSync },
|
||||
{ name: "design system A1 required tokens", run: checkDesignSystemA1RequiredTokens },
|
||||
{ name: "design system A2 required tokens", run: checkDesignSystemA2RequiredTokens },
|
||||
|
||||
Reference in New Issue
Block a user