Skip to content

fix: getWebStyles not return default html styles#440

Merged
Brentlok merged 7 commits intomainfrom
fix/getWebStyles
Mar 6, 2026
Merged

fix: getWebStyles not return default html styles#440
Brentlok merged 7 commits intomainfrom
fix/getWebStyles

Conversation

@Brentlok
Copy link
Contributor

@Brentlok Brentlok commented Mar 6, 2026

fixes #411

Summary by CodeRabbit

  • Refactor

    • Safer web-style extraction with improved selector handling, more reliable media-query tracking, and automatic pruning of stale stylesheet rules — public APIs unchanged.
  • Bug Fixes

    • Responsive and dynamic stylesheet changes now update more reliably (including attribute-driven changes and media-query updates).
  • Tests

    • Expanded end-to-end tests to use theme color constants, added combined-class scenarios, and tightened expectations for defaults.

@coderabbitai
Copy link

coderabbitai bot commented Mar 6, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Registers and tracks active CSSStyleRule objects (including media-query membership), prunes stale rules, listens for media-query changes, and switches getWebStyles to derive styles by scanning CSSListener.activeRules and safe selector-matching against a dummy element; tests updated for color and typography cases.

Changes

Cohort / File(s) Summary
CSS Listener Core
packages/uniwind/src/core/web/cssListener.ts
Added public activeRules: Set<CSSStyleRule>; renamed registeredRulesregisteredRulesMediaQueries: Map<string, MediaQueryList>; MutationObserver now watches childList and attributes; added pruneStaleRules(), isSupportsRule(), and toggleRule(); addMediaQueriesDeep/addMediaQuery now register rules with media queries, cache MediaQueryList objects, and update activeRules on MQL changes.
Style Extraction Logic
packages/uniwind/src/core/web/getWebStyles.ts
Replaced diff-based computed-style approach with getActiveStylesForClass(className) that iterates CSSListener.activeRules, strips pseudo-elements, safely matches selectors via a dummy element (using CSS.escape), collects declarations from matching rules into an extracted style object, and converts values to RN-style; added guards for missing className/dummy and try/catch for unparseable selectors.
Tests (e2e)
packages/uniwind/tests/e2e/getWebStyles.test.ts
Expanded test suite: import theme color constants, replace hardcoded color with TW_RED_500, add combined background+color test, add text-base assertions for fontSize and lineHeight, and add case expecting empty output for max-w-0:text-base.

Sequence Diagram(s)

sequenceDiagram
  participant Caller as Caller
  participant GW as getWebStyles
  participant CL as CSSListener
  participant Dummy as DummyElem
  participant MQL as MediaQueryList

  Caller->>GW: request styles for className
  GW->>CL: read activeRules
  CL->>CL: pruneStaleRules() (on init)
  loop for each CSSStyleRule in activeRules
    GW->>Dummy: set selector (strip pseudos) & apply className
    Dummy-->>GW: matches? (try/catch)
    alt match
      GW->>GW: collect rule.declarations -> parse to RN-style
    end
  end
  par media-query monitoring
    CL->>MQL: subscribe to MediaQueryList events
    MQL-->>CL: change -> toggleRule(mqList, rule)
  end
  GW-->>Caller: combined RN-style object
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I hopped through rules with careful ears,

Pruned the stale and chased the gears,
I matched each selector, kept what’s true,
Tuned media queries and fetched the hue,
A tidy hop — styles found anew.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Out of Scope Changes check ❓ Inconclusive While the PR addresses the linked issue objectives, the implementation goes significantly beyond the stated scope with major architectural changes to how CSS rules are tracked and processed. Clarify whether the refactoring of CSSListener (activeRules set, registeredRulesMediaQueries, pruneStaleRules, toggleRule) represents necessary architectural improvements or scope creep beyond fixing text-base style extraction.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix: getWebStyles not return default html styles' accurately reflects the main objective: fixing getWebStyles to properly return default HTML styles including inherited properties like fontSize and lineHeight.
Linked Issues check ✅ Passed The PR successfully implements the core requirement from issue #432: modifying getWebStyles to return fontSize and lineHeight for text utility classes, with test coverage added to validate the fix.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/getWebStyles

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/uniwind/src/core/web/cssListener.ts`:
- Around line 5-8: registeredRules is being used as an append-only cache which
can contain stale rules; update the implementation so the active-rule source of
truth reflects rule liveness and media state instead of blindly iterating
registeredRules. Either (A) clear and rebuild registeredRules during
re-initialization (and on stylesheet mutations) by scanning document.styleSheets
and repopulating only currently applicable CSSStyleRule entries, or (B) change
registeredRules into a Map<CSSStyleRule, MediaQueryList|boolean> (or use the
existing registeredRulesMediaQueries map) and update entries when listeners (the
listeners Map and MediaQueryList callbacks) fire or when stylesheets are removed
so iteration in getWebStyles uses only entries whose media match and whose
stylesheet is still in document.styleSheets; update code paths that add rules
(e.g., where lines 143-151 register rules) to set the liveness/media state
instead of appending blindly.

In `@packages/uniwind/src/core/web/getWebStyles.ts`:
- Around line 26-35: The comparison against rule.selectorText is using raw class
tokens and misses CSSOM-escaped selectors; in the CSSListener.registeredRules
loop update the mightMatch logic in getWebStyles.ts to escape each class token
(use CSS.escape on values from classNames derived from className) before
checking selector.includes(`.${escapedToken}`) so variant/escaped Tailwind
tokens (e.g. sm:text-base, w-1/2) are detected prior to calling dummy.matches();
modify the block that computes mightMatch (and any upstream uses of the raw
token) to use the escaped token instead.
- Around line 19-20: The variable extractedStyles in getActiveStylesForClass is
typed as CSSStyleDeclaration which lacks a string index signature and prevents
dynamic assignment like extractedStyles[camelCaseName] = propertyValue; change
extractedStyles to a plain string-keyed map (e.g., Record<string, string>) so
you can assign dynamic properties, update any related type annotations/usages in
getActiveStylesForClass to use the new map type, and ensure returns and
consumers expect a Record<string, string> instead of CSSStyleDeclaration.

In `@packages/uniwind/tests/e2e/getWebStyles.test.ts`:
- Around line 74-77: Update the test named "text-base -> fontSize 16px" (the
test function calling getWebStyles(page, 'text-base')) to also assert the
computed lineHeight; after obtaining styles from getWebStyles, add an assertion
that styles.lineHeight equals the expected value (e.g., '24px' for text-base at
16px fontSize) so both fontSize and lineHeight regressions are covered.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 13c658ac-ee32-4ba2-9630-d05c09d3e936

📥 Commits

Reviewing files that changed from the base of the PR and between dd5f432 and 8cd77d0.

📒 Files selected for processing (3)
  • packages/uniwind/src/core/web/cssListener.ts
  • packages/uniwind/src/core/web/getWebStyles.ts
  • packages/uniwind/tests/e2e/getWebStyles.test.ts

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
packages/uniwind/src/core/web/getWebStyles.ts (2)

43-48: Redundant camelCase conversion.

The property name is converted to camelCase at line 45, then the same conversion is applied again at lines 80-82 in getWebStyles. Since keys are already camelCased, the second pass is a no-op. Consider removing one of the conversions.

♻️ Option A: Remove conversion in getActiveStylesForClass (simpler helper)
             for (const propertyName of rule.style) {
                 const propertyValue = computedStyles.getPropertyValue(propertyName)
-                const camelCaseName = propertyName.replace(/-([a-z])/g, (g) => (g[1] ?? '').toUpperCase())
 
-                extractedStyles[camelCaseName] = propertyValue
+                extractedStyles[propertyName] = propertyValue
             }
♻️ Option B: Remove conversion in getWebStyles (avoid redundant iteration)
-    return Object.fromEntries(
-        Object.entries(computedStyles)
-            .map(([key, value]) => {
-                const parsedKey = key[0] === '-'
-                    ? key
-                    : key.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())
-
-                return [
-                    parsedKey,
-                    parseCSSValue(value),
-                ]
-            }),
-    )
+    return Object.fromEntries(
+        Object.entries(computedStyles)
+            .map(([key, value]) => [key, parseCSSValue(value)]),
+    )

Also applies to: 77-88

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/uniwind/src/core/web/getWebStyles.ts` around lines 43 - 48, There’s
a duplicated camelCase conversion of CSS property names between getWebStyles and
getActiveStylesForClass; remove one conversion to avoid redundant work — either
stop converting to camelCase in the first loop inside getWebStyles (remove
creation/assignment of camelCaseName and assign extractedStyles[propertyName] =
propertyValue) or stop converting in getActiveStylesForClass and rely on
getWebStyles to emit camelCased keys; update usages that expect camelCased keys
accordingly (look for variables propertyName, camelCaseName, extractedStyles,
and the functions getWebStyles and getActiveStylesForClass) so keys remain
consistent across the codebase.

19-20: Consider using Record<string, string> for type safety.

Since getPropertyValue() always returns a string, using Record<string, string> would be more precise than Record<string, any> and improve type safety downstream in getWebStyles.

♻️ Suggested fix
 const getActiveStylesForClass = (className: string) => {
-    const extractedStyles = {} as Record<string, any>
+    const extractedStyles: Record<string, string> = {}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/uniwind/src/core/web/getWebStyles.ts` around lines 19 - 20, The
extractedStyles object in getActiveStylesForClass is typed too loosely as
Record<string, any>; change its type to Record<string, string> to reflect that
window.getComputedStyle(...).getPropertyValue(...) returns strings, and update
any assignments in getActiveStylesForClass (and usages in getWebStyles) to
ensure values are string (e.g., cast/convert results of getPropertyValue to
string if necessary) so downstream consumers have correct, stricter typing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/uniwind/src/core/web/getWebStyles.ts`:
- Around line 43-48: There’s a duplicated camelCase conversion of CSS property
names between getWebStyles and getActiveStylesForClass; remove one conversion to
avoid redundant work — either stop converting to camelCase in the first loop
inside getWebStyles (remove creation/assignment of camelCaseName and assign
extractedStyles[propertyName] = propertyValue) or stop converting in
getActiveStylesForClass and rely on getWebStyles to emit camelCased keys; update
usages that expect camelCased keys accordingly (look for variables propertyName,
camelCaseName, extractedStyles, and the functions getWebStyles and
getActiveStylesForClass) so keys remain consistent across the codebase.
- Around line 19-20: The extractedStyles object in getActiveStylesForClass is
typed too loosely as Record<string, any>; change its type to Record<string,
string> to reflect that window.getComputedStyle(...).getPropertyValue(...)
returns strings, and update any assignments in getActiveStylesForClass (and
usages in getWebStyles) to ensure values are string (e.g., cast/convert results
of getPropertyValue to string if necessary) so downstream consumers have
correct, stricter typing.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 889a49f3-dd22-4c0f-b1c0-325909bc9f4c

📥 Commits

Reviewing files that changed from the base of the PR and between 8cd77d0 and 90fa00b.

📒 Files selected for processing (1)
  • packages/uniwind/src/core/web/getWebStyles.ts

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/uniwind/src/core/web/cssListener.ts (1)

182-186: ⚠️ Potential issue | 🟠 Major

Rules reusing a cached media query bypass match checks and change listeners.

When a rule shares its media query with a previously-processed rule, this early return skips:

  1. The initial match check (lines 190-194) — the rule stays in registeredRules regardless of whether the media query matches
  2. Registering a change listener (lines 200-208) — the rule won't be added/removed when the media query state changes

For example, with two rules under the same @media:

`@media` (max-width: 600px) { .foo { color: red; } }
`@media` (max-width: 600px) { .bar { color: blue; } }

The .bar rule will remain in registeredRules even when the viewport exceeds 600px.

🔧 Suggested fix
         if (cachedMediaQueryList) {
             this.classNameMediaQueryListeners.set(parsedClassName, cachedMediaQueryList)
 
+            if (cachedMediaQueryList.matches) {
+                this.registeredRules.add(rule)
+            } else {
+                this.registeredRules.delete(rule)
+            }
+
+            cachedMediaQueryList.addEventListener('change', () => {
+                if (cachedMediaQueryList.matches) {
+                    this.registeredRules.add(rule)
+                } else {
+                    this.registeredRules.delete(rule)
+                }
+            })
+
             return
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/uniwind/src/core/web/cssListener.ts` around lines 182 - 186, The
early return when cachedMediaQueryList exists (referencing
classNameMediaQueryListeners, parsedClassName and cachedMediaQueryList) skips
the per-rule match check and listener registration so subsequent rules sharing
the same media query are never evaluated or toggled; fix by removing the early
return and instead reuse the cached MediaQueryList object but continue to
perform the initial match check (the logic that updates registeredRules) and
attach the change listener for this parsedClassName just like the non-cached
path — ensure the mapping classNameMediaQueryListeners.set(parsedClassName,
cachedMediaQueryList) remains but follow with the same match and listener
registration code that handles adding/removing entries in registeredRules.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@packages/uniwind/src/core/web/cssListener.ts`:
- Around line 182-186: The early return when cachedMediaQueryList exists
(referencing classNameMediaQueryListeners, parsedClassName and
cachedMediaQueryList) skips the per-rule match check and listener registration
so subsequent rules sharing the same media query are never evaluated or toggled;
fix by removing the early return and instead reuse the cached MediaQueryList
object but continue to perform the initial match check (the logic that updates
registeredRules) and attach the change listener for this parsedClassName just
like the non-cached path — ensure the mapping
classNameMediaQueryListeners.set(parsedClassName, cachedMediaQueryList) remains
but follow with the same match and listener registration code that handles
adding/removing entries in registeredRules.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 888d3b29-c97a-4960-b2b3-6833200ee2e2

📥 Commits

Reviewing files that changed from the base of the PR and between 90fa00b and 80180c0.

📒 Files selected for processing (3)
  • packages/uniwind/src/core/web/cssListener.ts
  • packages/uniwind/src/core/web/getWebStyles.ts
  • packages/uniwind/tests/e2e/getWebStyles.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/uniwind/tests/e2e/getWebStyles.test.ts

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/uniwind/src/core/web/cssListener.ts (1)

194-203: Consider: Event listeners accumulate for rules sharing the same media query.

Each rule with the same media query condition adds a new listener to the cached MediaQueryList. While functionally correct (each toggles its specific rule), these listeners persist even after pruneStaleRules() removes the corresponding rules from activeRules.

For now this is likely acceptable—listeners are lightweight and toggleRule on a removed rule is harmless. If memory becomes a concern with large stylesheets, consider tracking listener references for cleanup.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/uniwind/src/core/web/cssListener.ts` around lines 194 - 203, Event
listeners are added to the same cached MediaQueryList for each rule but never
removed, so update the logic in cssListener.ts to track and remove listeners
when rules are pruned: when you call
cachedMediaQueryList.addEventListener('change', ...) (inside the branch where
classNameMediaQueryListeners is set for parsedClassName), capture the listener
callback and store it in a new map (e.g., mediaQueryListeners or entries on
classNameMediaQueryListeners keyed by parsedClassName or the media query string)
alongside the cachedMediaQueryList; then update pruneStaleRules() to look up and
call cachedMediaQueryList.removeEventListener('change', storedCallback) for any
removed parsedClassName/rule and delete the tracking entry, while keeping
toggleRule(cachedMediaQueryList, rule) behavior unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/uniwind/src/core/web/cssListener.ts`:
- Around line 212-215: The forEach callback for mediaQueryList currently uses a
concise arrow (listener => listener()) which returns the listener's result and
triggers the lint warning; change the callback to an explicit block body (e.g.,
listener => { listener(); }) or otherwise void the call so it returns undefined,
updating the call site where mediaQueryList.addEventListener('change', ...) uses
this.listeners.get(mediaQueryList)!.forEach(...) and then ensure
toggleRule(mediaQueryList, rule) remains invoked as before.
- Around line 172-178: When handling a CSSSupportsRule in addMediaQueriesDeep,
the branch that checks isSupportsRule(rule) and calls
addMediaQueriesDeep(rule.cssRules) must return control immediately to avoid
falling through into the later "'cssRules' in rule" branch and double-processing
the same nested rules; update addMediaQueriesDeep so that after calling
addMediaQueriesDeep(rule.cssRules) (when CSS.supports(rule.conditionText) is
true) you use continue/return to skip the subsequent "'cssRules' in rule" check
for that rule.

---

Nitpick comments:
In `@packages/uniwind/src/core/web/cssListener.ts`:
- Around line 194-203: Event listeners are added to the same cached
MediaQueryList for each rule but never removed, so update the logic in
cssListener.ts to track and remove listeners when rules are pruned: when you
call cachedMediaQueryList.addEventListener('change', ...) (inside the branch
where classNameMediaQueryListeners is set for parsedClassName), capture the
listener callback and store it in a new map (e.g., mediaQueryListeners or
entries on classNameMediaQueryListeners keyed by parsedClassName or the media
query string) alongside the cachedMediaQueryList; then update pruneStaleRules()
to look up and call cachedMediaQueryList.removeEventListener('change',
storedCallback) for any removed parsedClassName/rule and delete the tracking
entry, while keeping toggleRule(cachedMediaQueryList, rule) behavior unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7da91943-3b07-449a-87fe-c6be253e98f5

📥 Commits

Reviewing files that changed from the base of the PR and between 80180c0 and 40f1f7f.

📒 Files selected for processing (2)
  • packages/uniwind/src/core/web/cssListener.ts
  • packages/uniwind/src/core/web/getWebStyles.ts

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/uniwind/src/core/web/cssListener.ts`:
- Around line 19-20: Attribute-change mutations (handled in
scheduleInitialization/initialize) currently skip already-seen sheets using
processedStyleSheets, so toggling attributes like disabled/media/href leaves the
rule index stale; update initialize (or the attribute-mutation path in
scheduleInitialization) to invalidate or remove affected entries from
processedStyleSheets (or clear the cache) when the mutation.type is 'attributes'
for elements that can change stylesheet applicability, and then rebuild the
active rule index by re-scanning document.styleSheets (the same logic used by
getWebStyles.ts) so the toggled/modified <link> or <style> is re-evaluated;
ensure you reference processedStyleSheets, scheduleInitialization, and
initialize when making the change.
- Around line 87-95: pruneStaleRules currently removes dead CSSStyleRule objects
from activeRules but leaves MediaQueryList listeners (created in addMediaQuery)
holding references, letting toggleRule resurrect detached rules; fix by (a)
updating toggleRule to first verify rule.parentStyleSheet exists and is present
in document.styleSheets before toggling, and/or (b) during pruneStaleRules
iterate any stored MediaQueryList listeners (created in addMediaQuery) and call
removeEventListener / removeListener for those whose associated
rule.parentStyleSheet is missing, then clear their references from whatever
listener registry you use so they cannot re-add rules; reference
pruneStaleRules, toggleRule, and addMediaQuery to locate changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a3ba442a-4fcb-4664-909d-58c6f63cd65d

📥 Commits

Reviewing files that changed from the base of the PR and between 40f1f7f and fd94d2d.

📒 Files selected for processing (1)
  • packages/uniwind/src/core/web/cssListener.ts

@Brentlok Brentlok merged commit 461d04e into main Mar 6, 2026
2 checks passed
@Brentlok Brentlok deleted the fix/getWebStyles branch March 6, 2026 10:21
@github-actions
Copy link
Contributor

🚀 This pull request is included in v1.6.0. See Release v1.6.0 for release notes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

useResolveClassNames returns empty styles with the text-base className on the WEB

1 participant