Files
WRNexusJS/packages/styles/src/audit.ts
T
Clintchiz 2c960fc1dc
Quality / quality (ubuntu-latest) (push) Failing after 9m49s
Quality / quality (windows-latest) (push) Canceled after 0s
refactor: migrate legacy wire namespace to wrn
2026-08-12 18:51:15 +05:30

154 lines
4.9 KiB
TypeScript

export interface CssTokenAudit {
declared: string[];
used: string[];
missing: string[];
unused: string[];
}
/** Audit framework design-token declarations and var() references. */
export function auditWrnTokens(css: string): CssTokenAudit {
const declared = new Set<string>();
const used = new Set<string>();
for (const match of css.matchAll(/(--wrn-[A-Za-z0-9_-]+)\s*:/g)) declared.add(match[1]!);
for (const match of css.matchAll(/var\(\s*(--wrn-[A-Za-z0-9_-]+)/g)) used.add(match[1]!);
return {
declared: [...declared].sort(),
used: [...used].sort(),
missing: [...used].filter((token) => !declared.has(token)).sort(),
unused: [...declared].filter((token) => !used.has(token)).sort(),
};
}
export interface StyleSource {
path: string;
reason?: string;
}
/** Normalize/dedupe Tailwind scan sources without allowing line injection. */
export function normalizeStyleSources(values: readonly (string | StyleSource)[]): StyleSource[] {
const result = new Map<string, StyleSource>();
for (const value of values) {
const source = typeof value === "string" ? { path: value } : value;
const path = source.path.trim();
if (!path || /[\r\n]/.test(path)) continue;
result.set(path, { path, reason: source.reason });
}
return [...result.values()];
}
export function tailwindSourceDirectives(values: readonly (string | StyleSource)[]): string {
return normalizeStyleSources(values)
.map(({ path }) => `@source ${JSON.stringify(path)};`)
.join("\n");
}
export interface ContrastResult {
ratio: number;
level: "fail" | "aa-large" | "aa" | "aaa";
}
function hexRgb(value: string): [number, number, number] | null {
const hex = value.trim().replace(/^#/, "");
const normalized = hex.length === 3 ? [...hex].map((part) => part + part).join("") : hex;
if (!/^[0-9a-f]{6}$/i.test(normalized)) return null;
return [0, 2, 4].map((offset) => Number.parseInt(normalized.slice(offset, offset + 2), 16)) as [
number,
number,
number,
];
}
function luminance(value: string): number | null {
const rgb = hexRgb(value);
if (!rgb) return null;
const channels = rgb.map((channel) => {
const scaled = channel / 255;
return scaled <= 0.03928 ? scaled / 12.92 : ((scaled + 0.055) / 1.055) ** 2.4;
});
return 0.2126 * channels[0]! + 0.7152 * channels[1]! + 0.0722 * channels[2]!;
}
export function contrast(foreground: string, background: string): ContrastResult | null {
const left = luminance(foreground);
const right = luminance(background);
if (left === null || right === null) return null;
const ratio = (Math.max(left, right) + 0.05) / (Math.min(left, right) + 0.05);
return {
ratio: Math.round(ratio * 100) / 100,
level: ratio >= 7 ? "aaa" : ratio >= 4.5 ? "aa" : ratio >= 3 ? "aa-large" : "fail",
};
}
export interface CssPerformanceAuditIssue {
code: string;
severity: "error" | "warning" | "info";
message: string;
line?: number;
}
function cssLineAt(source: string, offset: number): number {
return source.slice(0, offset).split(/\r?\n/).length;
}
/** Detect CSS patterns that commonly increase style, paint, or compositing cost. */
export function auditCssPerformance(source: string): CssPerformanceAuditIssue[] {
const issues: CssPerformanceAuditIssue[] = [];
const checks: Array<{
pattern: RegExp;
code: string;
severity: CssPerformanceAuditIssue["severity"];
message: string;
}> = [
{
pattern: /transition\s*:\s*all\b/gi,
code: "WRN-CSS-TRANSITION-ALL",
severity: "warning",
message: "Avoid transition: all; list only properties that should animate.",
},
{
pattern: /backdrop-filter\s*:\s*[^;]*(?:blur\((?:[3-9]\d|\d{3,})px\))/gi,
code: "WRN-CSS-BACKDROP-BLUR",
severity: "warning",
message: "Large backdrop blur areas can be expensive to composite.",
},
{
pattern: /box-shadow\s*:[^;]*(?:,\s*[^;]+){4,}/gi,
code: "WRN-CSS-MANY-SHADOWS",
severity: "warning",
message: "Many layered shadows can increase paint cost.",
},
{
pattern: /(?:^|[},])\s*\*\s*(?:[,{])/gm,
code: "WRN-CSS-UNIVERSAL-SELECTOR",
severity: "info",
message: "Review universal selectors used in large DOM subtrees.",
},
];
for (const check of checks) {
for (const match of source.matchAll(check.pattern)) {
issues.push({
code: check.code,
severity: check.severity,
message: check.message,
line: cssLineAt(source, match.index ?? 0),
});
}
}
const keyframes = new Map<string, number>();
for (const match of source.matchAll(/@keyframes\s+([A-Za-z_][\w-]*)/g)) {
const name = match[1]!;
keyframes.set(name, (keyframes.get(name) ?? 0) + 1);
}
for (const [name, count] of keyframes) {
if (count > 1) {
issues.push({
code: "WRN-CSS-DUPLICATE-KEYFRAMES",
severity: "warning",
message: `@keyframes ${name} is declared ${count} times.`,
});
}
}
return issues;
}