Skip to content
Closed
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
57 changes: 53 additions & 4 deletions packages/uniwind/src/core/native/store.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable max-depth */
import { Dimensions, Platform } from 'react-native'
import { Orientation, StyleDependency } from '../../types'
import { ColorScheme, Orientation, StyleDependency } from '../../types'
import { UniwindListener } from '../listener'
import { ComponentState, CSSVariables, GenerateStyleSheetsCallback, RNStyle, Style, StyleSheets, ThemeName } from '../types'
import { cloneWithAccessors } from './native-utils'
Expand All @@ -17,6 +17,16 @@ class UniwindStoreBuilder {
vars = {} as Record<string, unknown>
runtimeThemeVariables = new Map<ThemeName, CSSVariables>()
private stylesheet = {} as StyleSheets
private varsWithMediaQueries = {} as Record<
string,
Array<{
value: unknown
minWidth: number | null
maxWidth: number | null
orientation: Orientation | null
colorScheme: ColorScheme | null
}>
>
private cache = new Map<string, StylesResult>()
private generateStyleSheetCallbackResult: ReturnType<GenerateStyleSheetsCallback> | null = null

Expand Down Expand Up @@ -53,8 +63,8 @@ class UniwindStoreBuilder {
return
}

const { scopedVars, stylesheet, vars } = config

const { scopedVars, stylesheet, vars, varsWithMediaQueries } = config
this.varsWithMediaQueries = varsWithMediaQueries ?? {}
this.generateStyleSheetCallbackResult = config
this.stylesheet = stylesheet
this.vars = vars
Expand All @@ -80,11 +90,50 @@ class UniwindStoreBuilder {
}
}

private resolveMediaQueryVars(dependencies: Set<StyleDependency>) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@Brentlok Kindly review this:

The coderabbitai has reviewed most part of the code, but I'll need an in-dept review of this method resolving media queries at runtime

const varsWithMediaQueries = Object.entries(this.varsWithMediaQueries)
const vars = varsWithMediaQueries.length > 0 ? { ...this.vars } : this.vars

for (const [varName, mqVariants] of varsWithMediaQueries) {
let bestMatch: { value: unknown; minWidth: number | null } | null = null

for (const variant of mqVariants) {
if (variant.orientation !== null) dependencies.add(StyleDependency.Orientation)
if (variant.maxWidth !== null || variant.minWidth !== null) dependencies.add(StyleDependency.Dimensions)
if (variant.colorScheme !== null) {
dependencies.add(StyleDependency.ColorScheme)
dependencies.add(StyleDependency.Theme)
}

if (
(variant.minWidth !== null && variant.minWidth > this.runtime.screen.width)
|| (variant.maxWidth !== null && variant.maxWidth !== Number.MAX_VALUE && variant.maxWidth < this.runtime.screen.width)
|| (variant.orientation !== null && this.runtime.orientation !== variant.orientation)
|| (variant.colorScheme !== null && this.runtime.currentThemeName !== variant.colorScheme)
) {
continue
}

if (bestMatch === null || (variant.minWidth ?? 0) > (bestMatch.minWidth ?? 0)) bestMatch = variant
}

if (bestMatch !== null) {
Object.defineProperty(vars, varName, {
configurable: true,
enumerable: true,
get: () => bestMatch.value,
})
}
}

return vars
}

private resolveStyles(classNames: string, state?: ComponentState) {
const result = {} as Record<string, any>
let vars = this.vars
const dependencies = new Set<StyleDependency>()
const bestBreakpoints = new Map<string, Style>()
let vars = this.resolveMediaQueryVars(dependencies)

for (const className of classNames.split(' ')) {
if (!(className in this.stylesheet)) {
Expand Down
10 changes: 10 additions & 0 deletions packages/uniwind/src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ export type GenerateStyleSheetsCallback = (rt: UniwindRuntime) => {
stylesheet: StyleSheets
vars: Record<string, unknown>
scopedVars: Partial<Record<string, Record<string, unknown>>>
varsWithMediaQueries?: Record<
string,
Array<{
value: unknown
minWidth: number | null
maxWidth: number | null
orientation: Orientation | null
colorScheme: ColorScheme | null
}>
>
}

type UserThemes = UniwindConfig extends { themes: infer T extends readonly string[] } ? T
Expand Down
6 changes: 5 additions & 1 deletion packages/uniwind/src/metro/compileVirtual.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Logger } from './logger'
import { polyfillWeb } from './polyfillWeb'
import { ProcessorBuilder } from './processor'
import { Platform, Polyfills } from './types'
import { serializeJSObject } from './utils'
import { serialize, serializeJSObject } from './utils'

type CompileVirtualConfig = {
cssPath: string
Expand Down Expand Up @@ -59,6 +59,9 @@ export const compileVirtual = async ({ css, cssPath, platform, themes, polyfills
serializeJSObject(scopedVars, (key, value) => `get "${key}"() { return ${value} }`),
]),
)
const varsWithMediaQueries = Object.entries(Processor.varsWithMediaQueries)
.map(([key, value]) => `"${key}": ${serialize(value)}`)
.join(',')
const serializedScopedVars = Object.entries(scopedVars)
.map(([scopedVarsName, scopedVars]) => `"${scopedVarsName}": ({ ${scopedVars} }),`)
.join('')
Expand All @@ -68,6 +71,7 @@ export const compileVirtual = async ({ css, cssPath, platform, themes, polyfills
'({',
`scopedVars: ({ ${serializedScopedVars} }),`,
`vars: ({ ${currentColorVar} ${vars} }),`,
`varsWithMediaQueries: ({ ${varsWithMediaQueries} }),`,
`stylesheet: ({ ${stylesheet} }),`,
'})',
].join('')
Expand Down
20 changes: 13 additions & 7 deletions packages/uniwind/src/metro/processor/mq.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MediaQuery, QueryFeatureFor_MediaFeatureId } from 'lightningcss'
import { MediaCondition, MediaQuery, QueryFeatureFor_MediaFeatureId } from 'lightningcss'
import { ColorScheme, Orientation } from '../../types'
import { MediaQueryResolver, Platform } from '../types'
import type { ProcessorBuilder } from './processor'
Expand All @@ -18,20 +18,26 @@ export class MQ {
return
}

if (condition?.type !== 'feature') {
return
}
if (condition) this.processCondition(condition, mq)
})

return mq
}

private processCondition(condition: MediaCondition, mq: MediaQueryResolver) {
if (condition.type === 'operation') {
condition.conditions.forEach(nestedCondition => {
this.processCondition(nestedCondition, mq)
})
} else if (condition.type === 'feature') {
if (condition.value.type === 'range') {
this.processWidthMediaQuery(condition.value, mq)
}

if (condition.value.type === 'plain') {
this.processPlainMediaQuery(condition.value, mq)
}
})

return mq
}
}
Comment on lines +27 to 41
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for MediaCondition import and type definition
rg -n "MediaCondition" packages/uniwind/src/metro/processor/mq.ts -C 2

Repository: uni-stack/uniwind

Length of output: 482


🏁 Script executed:

#!/bin/bash
# Search for Lightning CSS imports and any type definitions in the project
rg -n "from.*lightningcss" packages/uniwind/ --type ts

Repository: uni-stack/uniwind

Length of output: 1255


🏁 Script executed:

#!/bin/bash
# Check if there are any type definition files or interfaces for MediaCondition
fd -e d.ts -e ts | head -20 | xargs grep -l "MediaCondition" 2>/dev/null || echo "No type definition files found"

Repository: uni-stack/uniwind

Length of output: 91


🏁 Script executed:

#!/bin/bash
# Search for any documentation or comments about condition types
rg -n "condition\.type" packages/uniwind/ --type ts -C 1

Repository: uni-stack/uniwind

Length of output: 647


🌐 Web query:

Lightning CSS MediaCondition types operation feature not

💡 Result:

Short answer: Lightning CSS represents parsed media queries with a MediaCondition union whose variants are objects with a type field such as "type", "feature", "operation", and "not" (plus a few others like "style"/container-related variants). Each variant means:

  • type — a media type node (e.g., "screen", "print"). [1]
  • feature — a media feature or container feature (e.g., (min-width: 600px)); the node contains the feature name and value. [2]
  • operation — a logical grouping with an operator (and/or) and an array of sub-conditions (used for expressions like (A and B)). [2]
  • not — a negation node wrapping a single condition (e.g., not screen). [2]

See Lightning CSS docs and the TypeScript AST typings (lightningcss-wasm) for the exact shapes of the nodes. [1][2]

Sources:

  1. Lightning CSS docs / crate (media-query and AST modules). [1]
  2. lightningcss-wasm TypeScript AST diff / d.ts showing MediaCondition variants ("type", "feature", "operation", "not", etc.). [2]

🌐 Web query:

lightningcss MediaCondition type definition "operation" "feature" "not"

💡 Result:

In Lightning CSS the MediaCondition AST (media_query::MediaCondition) has three node kinds you asked about:

  • type: "feature" — a single media/container feature (e.g. (width >= 600px) or (prefers-reduced-motion: reduce)). Represents a QueryFeature node containing the feature name and value. [1]

  • type: "operation" — a boolean operation combining conditions using "and" or "or" (e.g. (min-width: 600px) and (orientation: landscape)). Holds the operator and left/right child MediaCondition nodes. [1]

  • type: "not" — a negation of a condition (e.g. not (hover: hover)). Contains the single child MediaCondition being negated. [1]

Sources: docs for lightningcss media_query (MediaCondition / QueryFeature). [1][2]

References
[1] lightningcss media_query (MediaCondition / QueryFeature) — docs.rs. (docs.rs)
[2] lightningcss project README (features list: media queries support) — GitHub. (github.com)

Citations:


🏁 Script executed:

#!/bin/bash
# Check if there are any tests or other usages of processCondition to understand expected behavior
rg -n "processCondition\|MediaCondition" packages/uniwind/ --type ts -B 2 -A 5 | head -60

Repository: uni-stack/uniwind

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Search for any handling of 'not' or 'type' condition types in the entire codebase
rg -n "condition\.type.*=.*['\"]not['\"]|condition\.type.*=.*['\"]type['\"]" packages/uniwind/ --type ts

Repository: uni-stack/uniwind

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Check the full context of the processCondition method and surrounding code
cat -n packages/uniwind/src/metro/processor/mq.ts | head -50

Repository: uni-stack/uniwind

Length of output: 2202


Handle missing MediaCondition types: 'not' and 'type'.

The current implementation only processes 'operation' and 'feature' condition types, but Lightning CSS MediaCondition is a union that also includes 'type' (media type constraints like screen, print) and 'not' (negations like not (hover)). These types are silently ignored, which means media queries using negations or type constraints won't be processed correctly.

Add handling for both:

  • 'not' type: recursively process the wrapped condition
  • 'type' type: decide whether media type constraints need processing or can be skipped with a comment explaining why


private processWidthMediaQuery(query: QueryFeatureFor_MediaFeatureId & { type: 'range' }, mq: MediaQueryResolver) {
Expand Down
67 changes: 63 additions & 4 deletions packages/uniwind/src/metro/processor/processor.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Declaration, MediaQuery, Rule, transform } from 'lightningcss'
import { Polyfills, ProcessMetaValues } from '../types'
import { MediaQueryResolver, Polyfills, ProcessMetaValues } from '../types'
import { Color } from './color'
import { CSS } from './css'
import { Functions } from './functions'
Expand All @@ -12,6 +12,7 @@ export class ProcessorBuilder {
stylesheets = {} as Record<string, Array<any>>
vars = {} as Record<string, any>
scopedVars = {} as Record<string, Record<string, any>>
varsWithMediaQueries = {} as Record<string, Array<any>>
CSS = new CSS(this)
RN = new RN(this)
Var = new Var(this)
Expand Down Expand Up @@ -54,6 +55,20 @@ export class ProcessorBuilder {
})
}

private storeVarWithMediaQuery(varName: string, value: any, mq: MediaQueryResolver) {
if (!Array.isArray(this.varsWithMediaQueries[varName])) {
this.varsWithMediaQueries[varName] = []
}

this.varsWithMediaQueries[varName].push({
value,
minWidth: mq.minWidth,
maxWidth: mq.maxWidth,
orientation: mq.orientation ? `'${mq.orientation}'` : null,
colorScheme: mq.colorScheme ? `'${mq.colorScheme}'` : null,
})
}

private addDeclaration(declaration: Declaration, important = false) {
const isVar = this.declarationConfig.root || this.declarationConfig.className === null
const mq = this.MQ.processMediaQueries(this.declarationConfig.mediaQueries)
Expand Down Expand Up @@ -91,7 +106,20 @@ export class ProcessorBuilder {
}

if (declaration.property === 'unparsed') {
style[declaration.value.propertyId.property] = this.CSS.processValue(declaration.value.value)
const varName = declaration.value.propertyId.property
const processedValue = this.CSS.processValue(declaration.value.value)

if (isVar) {
const hasMediaQuery = mq.minWidth !== 0 || mq.maxWidth !== Number.MAX_VALUE || mq.orientation !== null || mq.colorScheme !== null

if (hasMediaQuery) {
this.storeVarWithMediaQuery(varName, processedValue, mq)
} else {
style[varName] = processedValue
}
} else {
style[varName] = processedValue
}

if (!isVar && important) {
style.importantProperties.push(declaration.value.propertyId.property)
Expand All @@ -101,7 +129,20 @@ export class ProcessorBuilder {
}

if (declaration.property === 'custom') {
style[declaration.value.name] = this.CSS.processValue(declaration.value.value)
const varName = declaration.value.name
const processedValue = this.CSS.processValue(declaration.value.value)

if (isVar) {
const hasMediaQuery = mq.minWidth !== 0 || mq.maxWidth !== Number.MAX_VALUE || mq.orientation !== null || mq.colorScheme !== null

if (hasMediaQuery) {
this.storeVarWithMediaQuery(varName, processedValue, mq)
} else {
style[varName] = processedValue
}
} else {
style[varName] = processedValue
}

if (!isVar && important) {
style.importantProperties.push(declaration.value.name)
Expand All @@ -110,7 +151,20 @@ export class ProcessorBuilder {
return
}

style[declaration.property] = this.CSS.processValue(declaration.value)
const varName = declaration.property
const processedValue = this.CSS.processValue(declaration.value)

if (isVar) {
const hasMediaQuery = mq.minWidth !== 0 || mq.maxWidth !== Number.MAX_VALUE || mq.orientation !== null || mq.colorScheme !== null

if (hasMediaQuery) {
this.storeVarWithMediaQuery(varName, processedValue, mq)
} else {
style[varName] = processedValue
}
} else {
style[varName] = processedValue
}

if (!isVar && important) {
style.importantProperties.push(declaration.property)
Expand Down Expand Up @@ -226,6 +280,11 @@ export class ProcessorBuilder {
this.declarationConfig = this.getDeclarationConfig()
})

this.declarationConfig.mediaQueries.splice(
this.declarationConfig.mediaQueries.length - mediaQueries.length,
mediaQueries.length,
)

return
}

Expand Down
4 changes: 2 additions & 2 deletions packages/uniwind/src/metro/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ export type UniwindConfig = {
}

export type MediaQueryResolver = {
maxWidth: any
minWidth: any
maxWidth: number | null
minWidth: number | null
platform: Platform | null
rtl: boolean | null
important: boolean
Expand Down
19 changes: 19 additions & 0 deletions packages/uniwind/tests/media-queries/color-scheme.test.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
@import 'tailwindcss';
@import 'uniwind';

@layer theme {
:root {
--color-primary: #000000;
}
}

@media (prefers-color-scheme: dark) {
:root {
--color-primary: #ffffff;
}
}

.text-color-primary {
color: var(--color-primary);
}

Loading