Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/preflight-full-validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'helixir': patch
'@helixir/core': patch
---

Enhance styling_preflight to run all 7 CSS validators in a single call β€” adds token fallback, scope, shorthand, color contrast, and specificity checks alongside existing shadow DOM and theme compatibility checks
76 changes: 74 additions & 2 deletions custom-elements.json
Original file line number Diff line number Diff line change
Expand Up @@ -4164,6 +4164,36 @@
"kind": "javascript-module",
"path": "packages/core/src/handlers/scope-checker.ts",
"declarations": [
{
"kind": "function",
"name": "checkCssScopeFromMeta",
"return": {
"type": {
"text": "ScopeCheckResult"
}
},
"parameters": [
{
"name": "css",
"type": {
"text": "string"
}
},
{
"name": "tagName",
"type": {
"text": "string"
}
},
{
"name": "cssProperties",
"type": {
"text": "Array<{ name: string }>"
}
}
],
"description": "Core implementation accepting a pre-built set of known tokens.\nUsed by both the CEM-based entry point and the preflight (which already has metadata)."
},
{
"kind": "function",
"name": "checkCssScope",
Expand Down Expand Up @@ -4191,10 +4221,19 @@
"text": "Cem"
}
}
]
],
"description": "CEM-based entry point β€” parses the CEM to extract known tokens,\nthen delegates to the core implementation."
}
],
"exports": [
{
"kind": "js",
"name": "checkCssScopeFromMeta",
"declaration": {
"name": "checkCssScopeFromMeta",
"module": "packages/core/src/handlers/scope-checker.ts"
}
},
{
"kind": "js",
"name": "checkCssScope",
Expand Down Expand Up @@ -4888,6 +4927,30 @@
"kind": "javascript-module",
"path": "packages/core/src/handlers/token-fallback-checker.ts",
"declarations": [
{
"kind": "function",
"name": "checkTokenFallbacksFromMeta",
"return": {
"type": {
"text": "TokenFallbackResult"
}
},
"parameters": [
{
"name": "cssText",
"type": {
"text": "string"
}
},
{
"name": "knownTokens",
"type": {
"text": "Set<string>"
}
}
],
"description": "Core implementation accepting a pre-built set of known tokens.\nUsed by both the CEM-based entry point and the preflight (which already has metadata)."
},
{
"kind": "function",
"name": "checkTokenFallbacks",
Expand Down Expand Up @@ -4915,10 +4978,19 @@
"text": "Cem"
}
}
]
],
"description": "CEM-based entry point β€” parses the CEM to extract known tokens,\nthen delegates to the core implementation."
}
],
"exports": [
{
"kind": "js",
"name": "checkTokenFallbacksFromMeta",
"declaration": {
"name": "checkTokenFallbacksFromMeta",
"module": "packages/core/src/handlers/token-fallback-checker.ts"
}
},
{
"kind": "js",
"name": "checkTokenFallbacks",
Expand Down
22 changes: 19 additions & 3 deletions packages/core/src/handlers/scope-checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,16 @@ function parseCssBlocks(css: string): CssBlock[] {

// ─── Main Entry Point ───────────────────────────────────────────────────────

export function checkCssScope(css: string, tagName: string, cem: Cem): ScopeCheckResult {
const meta = parseCem(tagName, cem);
const knownTokens = new Set(meta.cssProperties.map((p) => p.name));
/**
* Core implementation accepting a pre-built set of known tokens.
* Used by both the CEM-based entry point and the preflight (which already has metadata).
*/
export function checkCssScopeFromMeta(
css: string,
tagName: string,
cssProperties: Array<{ name: string }>,
): ScopeCheckResult {
const knownTokens = new Set(cssProperties.map((p) => p.name));

const blocks = parseCssBlocks(css);
const issues: ScopeIssue[] = [];
Expand Down Expand Up @@ -114,3 +121,12 @@ export function checkCssScope(css: string, tagName: string, cem: Cem): ScopeChec
clean: issues.length === 0,
};
}

/**
* CEM-based entry point β€” parses the CEM to extract known tokens,
* then delegates to the core implementation.
*/
export function checkCssScope(css: string, tagName: string, cem: Cem): ScopeCheckResult {
const meta = parseCem(tagName, cem);
return checkCssScopeFromMeta(css, tagName, meta.cssProperties);
}
99 changes: 89 additions & 10 deletions packages/core/src/handlers/styling-preflight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@ import type { ComponentMetadata } from './cem.js';
import { resolveCssApi, type CssApiResolution } from './css-api-resolver.js';
import { checkShadowDomUsage } from './shadow-dom-checker.js';
import { checkThemeCompatibility } from './theme-checker.js';
import { checkCssShorthand } from './shorthand-checker.js';
import { checkColorContrast } from './color-contrast-checker.js';
import { checkCssSpecificity } from './specificity-checker.js';
import { buildCssSnippet } from './styling-diagnostics.js';
import { checkTokenFallbacksFromMeta } from './token-fallback-checker.js';
import { checkCssScopeFromMeta } from './scope-checker.js';

// ─── Types ───────────────────────────────────────────────────────────────────

Expand Down Expand Up @@ -60,9 +65,10 @@ export function runStylingPreflight(input: PreflightInput): PreflightResult {
// 1. Resolve CSS references against CEM
const resolution = resolveCssApi(css, meta, html);

// 2. Run shadow DOM validation (if CSS is non-empty)
// 2. Run all CSS validators (if CSS is non-empty)
if (css.trim()) {
try {
// Shadow DOM anti-patterns
safeRun(() => {
const shadowResult = checkShadowDomUsage(css, meta.tagName, meta);
for (const issue of shadowResult.issues) {
issues.push({
Expand All @@ -73,12 +79,10 @@ export function runStylingPreflight(input: PreflightInput): PreflightResult {
suggestion: issue.suggestion,
});
}
} catch {
// Shadow DOM check failed β€” skip
}
});

// 3. Run theme compatibility check
try {
// Theme compatibility (hardcoded colors, mixed token sources, dark mode shadows)
safeRun(() => {
const themeResult = checkThemeCompatibility(css);
for (const issue of themeResult.issues) {
issues.push({
Expand All @@ -88,9 +92,74 @@ export function runStylingPreflight(input: PreflightInput): PreflightResult {
line: issue.line,
});
}
} catch {
// Theme check failed β€” skip
}
});

// Token fallbacks (var() without fallbacks, hardcoded colors on theme properties)
safeRun(() => {
const knownTokens = new Set(meta.cssProperties.map((p) => p.name));
const fallbackResult = checkTokenFallbacksFromMeta(css, knownTokens);
for (const issue of fallbackResult.issues) {
issues.push({
severity: 'warning',
category: 'tokenFallbacks',
message: issue.message,
line: issue.line,
});
}
});

// Scope validation (component tokens on :root instead of host)
safeRun(() => {
const scopeResult = checkCssScopeFromMeta(css, meta.tagName, meta.cssProperties);
for (const issue of scopeResult.issues) {
issues.push({
severity: 'warning',
category: 'scope',
message: issue.message,
line: issue.line,
});
}
});

// Shorthand + var() risky combinations
safeRun(() => {
const shorthandResult = checkCssShorthand(css);
for (const issue of shorthandResult.issues) {
issues.push({
severity: 'warning',
category: 'shorthand',
message: issue.message,
line: issue.line,
suggestion: issue.suggestion,
});
}
});

// Color contrast issues (low-contrast pairs, mixed sources)
safeRun(() => {
const contrastResult = checkColorContrast(css);
for (const issue of contrastResult.issues) {
issues.push({
severity: 'warning',
category: 'colorContrast',
message: issue.message,
line: issue.line,
});
}
});

// CSS specificity anti-patterns (!important, ID selectors, deep nesting)
safeRun(() => {
const specResult = checkCssSpecificity(css);
for (const issue of specResult.issues) {
issues.push({
severity: 'info',
category: 'specificity',
message: issue.message,
line: issue.line,
});
}
});
}

// 4. Build the component API summary
Expand Down Expand Up @@ -118,6 +187,16 @@ export function runStylingPreflight(input: PreflightInput): PreflightResult {
};
}

// ─── Safe Runner ─────────────────────────────────────────────────────────────

function safeRun(fn: () => void): void {
try {
fn();
} catch {
// Individual checker failed β€” skip and continue with other checks
}
}

// ─── Verdict Builder ────────────────────────────────────────────────────────

function buildVerdict(resolution: CssApiResolution, issues: PreflightIssue[]): string {
Expand Down
26 changes: 20 additions & 6 deletions packages/core/src/handlers/token-fallback-checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,14 +171,14 @@ function parseCssDeclarations(css: string): CssPropertyDecl[] {

// ─── Main Entry Point ───────────────────────────────────────────────────────

export function checkTokenFallbacks(
/**
* Core implementation accepting a pre-built set of known tokens.
* Used by both the CEM-based entry point and the preflight (which already has metadata).
*/
export function checkTokenFallbacksFromMeta(
cssText: string,
tagName: string,
cem: Cem,
knownTokens: Set<string>,
): TokenFallbackResult {
const meta = parseCem(tagName, cem);
const knownTokens = new Set(meta.cssProperties.map((p) => p.name));

const declarations = parseCssDeclarations(cssText);
const issues: TokenFallbackIssue[] = [];
let totalVarCalls = 0;
Expand Down Expand Up @@ -228,3 +228,17 @@ export function checkTokenFallbacks(
clean: issues.length === 0,
};
}

/**
* CEM-based entry point β€” parses the CEM to extract known tokens,
* then delegates to the core implementation.
*/
export function checkTokenFallbacks(
cssText: string,
tagName: string,
cem: Cem,
): TokenFallbackResult {
const meta = parseCem(tagName, cem);
const knownTokens = new Set(meta.cssProperties.map((p) => p.name));
return checkTokenFallbacksFromMeta(cssText, knownTokens);
}
Loading
Loading