From 73c2f22b63b158a339ac52f6a520512c94adad48 Mon Sep 17 00:00:00 2001 From: brkalow Date: Fri, 20 Mar 2026 13:19:04 -0500 Subject: [PATCH] fix: exclude internal Clerk classes from structural CSS detection Only user-authored customization targeting Clerk's public APIs should trigger the structural CSS warning. Internal classes (e.g., .cl-internal-*) generated by Emotion are implementation details and shouldn't be flagged. However, if a user explicitly references .cl-internal-* in their appearance.elements configuration, that's still a stability risk and will be warned about. Co-Authored-By: Claude Haiku 4.5 --- .../detectClerkStylesheetUsage.test.ts | 36 +++++++++++++++++++ ...rnAboutCustomizationWithoutPinning.test.ts | 16 +++++++++ packages/ui/src/utils/cssPatterns.ts | 5 ++- .../src/utils/detectClerkStylesheetUsage.ts | 13 +++++-- 4 files changed, 67 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/utils/__tests__/detectClerkStylesheetUsage.test.ts b/packages/ui/src/utils/__tests__/detectClerkStylesheetUsage.test.ts index 727abc0dbd7..1b194fc4ec2 100644 --- a/packages/ui/src/utils/__tests__/detectClerkStylesheetUsage.test.ts +++ b/packages/ui/src/utils/__tests__/detectClerkStylesheetUsage.test.ts @@ -177,6 +177,42 @@ describe('detectStructuralClerkCss', () => { }); }); + describe('should NOT flag .cl-internal-* classes', () => { + test('.cl-internal- class with :has() selector', () => { + mockStyleSheets([ + createMockStyleSheet([ + createMockStyleRule( + '.cl-internal-go4bxw:has(button:focus-visible)', + '.cl-internal-go4bxw:has(button:focus-visible) { }', + ), + ]), + ]); + + const hits = detectStructuralClerkCss(); + expect(hits).toHaveLength(0); + }); + + test('.cl-internal- class with descendant selector', () => { + mockStyleSheets([ + createMockStyleSheet([createMockStyleRule('.cl-internal-o2kwkh ul', '.cl-internal-o2kwkh ul { }')]), + ]); + + const hits = detectStructuralClerkCss(); + expect(hits).toHaveLength(0); + }); + + test('tag with descendant .cl-internal- class', () => { + mockStyleSheets([ + createMockStyleSheet([ + createMockStyleRule('button:hover .cl-internal-gxb76v', 'button:hover .cl-internal-gxb76v { }'), + ]), + ]); + + const hits = detectStructuralClerkCss(); + expect(hits).toHaveLength(0); + }); + }); + describe('should handle CORS-blocked stylesheets gracefully', () => { test('skips stylesheets that throw on cssRules access', () => { const blockedSheet = { diff --git a/packages/ui/src/utils/__tests__/warnAboutCustomizationWithoutPinning.test.ts b/packages/ui/src/utils/__tests__/warnAboutCustomizationWithoutPinning.test.ts index dedb716c7b7..f689d471fa1 100644 --- a/packages/ui/src/utils/__tests__/warnAboutCustomizationWithoutPinning.test.ts +++ b/packages/ui/src/utils/__tests__/warnAboutCustomizationWithoutPinning.test.ts @@ -139,6 +139,22 @@ describe('warnAboutCustomizationWithoutPinning', () => { }); describe('appearance.elements - should warn', () => { + test('for nested selector referencing .cl-internal- class', () => { + warnAboutCustomizationWithoutPinning({ + appearance: { + elements: { + card: { + '& .cl-internal-abc123': { padding: '20px' }, + }, + }, + }, + }); + + expect(logger.warnOnce).toHaveBeenCalledTimes(1); + const message = getWarningMessage(); + expect(message).toContain('elements.card "& .cl-internal-abc123"'); + }); + test('for nested selector with .cl- class reference', () => { warnAboutCustomizationWithoutPinning({ appearance: { diff --git a/packages/ui/src/utils/cssPatterns.ts b/packages/ui/src/utils/cssPatterns.ts index 4f402bfafe4..7f5e9d29cf0 100644 --- a/packages/ui/src/utils/cssPatterns.ts +++ b/packages/ui/src/utils/cssPatterns.ts @@ -3,9 +3,12 @@ * Used by both stylesheet detection and CSS-in-JS analysis. */ -// Matches .cl- class selectors (Clerk's internal class prefix) +// Matches .cl- class selectors (Clerk's class prefix) export const CLERK_CLASS_RE = /\.cl-[A-Za-z0-9_-]+/; +// Matches .cl-internal- class selectors (Clerk's generated internal classes, not user-facing) +export const CLERK_INTERNAL_CLASS_RE = /\.cl-internal-[A-Za-z0-9_-]+/g; + // Matches attribute selectors targeting cl- classes (e.g., [class^="cl-"]) export const CLERK_ATTR_RE = /\[\s*class\s*(\^=|\*=|\$=)\s*["']?[^"'\]]*cl-[^"'\]]*["']?\s*\]/i; diff --git a/packages/ui/src/utils/detectClerkStylesheetUsage.ts b/packages/ui/src/utils/detectClerkStylesheetUsage.ts index c193e66ea62..04a4839c2ce 100644 --- a/packages/ui/src/utils/detectClerkStylesheetUsage.ts +++ b/packages/ui/src/utils/detectClerkStylesheetUsage.ts @@ -1,4 +1,4 @@ -import { CLERK_ATTR_RE, CLERK_CLASS_RE, HAS_RE, POSITIONAL_PSEUDO_RE } from './cssPatterns'; +import { CLERK_ATTR_RE, CLERK_CLASS_RE, CLERK_INTERNAL_CLASS_RE, HAS_RE, POSITIONAL_PSEUDO_RE } from './cssPatterns'; type ClerkStructuralHit = { stylesheetHref: string | null; @@ -7,8 +7,17 @@ type ClerkStructuralHit = { reason: string[]; }; +/** + * Strips .cl-internal-* classes from a selector so they don't trigger detection. + * These are Clerk's own generated classes, not user-facing customization points. + */ +function stripInternalClasses(selector: string): string { + return selector.replace(CLERK_INTERNAL_CLASS_RE, ''); +} + function isProbablyClerkSelector(selector: string): boolean { - return CLERK_CLASS_RE.test(selector) || CLERK_ATTR_RE.test(selector); + const stripped = stripInternalClasses(selector); + return CLERK_CLASS_RE.test(stripped) || CLERK_ATTR_RE.test(stripped); } // Split by commas safely-ish (won't perfectly handle :is(...) with commas, but good enough)