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
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
}
}

private processWidthMediaQuery(query: QueryFeatureFor_MediaFeatureId & { type: 'range' }, mq: MediaQueryResolver) {
Expand Down
96 changes: 74 additions & 22 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 @@ -21,6 +21,9 @@ export class ProcessorBuilder {
Functions = new Functions(this)
meta = {} as ProcessMetaValues

private varsWithMediaQueries = {} as Record<string, Array<any>>
private pendingVarReferences = new Map<string, Array<string>>()

private declarationConfig = this.getDeclarationConfig()

constructor(private readonly themes: Array<string>, readonly polyfills: Polyfills | undefined) {
Expand All @@ -32,11 +35,27 @@ export class ProcessorBuilder {
filename: 'tailwind.css',
code: Buffer.from(css),
visitor: {
StyleSheet: styleSheet =>
StyleSheet: styleSheet => {
styleSheet.rules.forEach(rule => {
this.declarationConfig = this.getDeclarationConfig()
this.parseRuleRec(rule)
}),
})

for (const [className, varNames] of this.pendingVarReferences) {
for (const varName of varNames) {
const varStyles = this.varsWithMediaQueries[varName]
if (!varStyles || varStyles.length === 0) {
continue
}

for (const varStyle of varStyles) {
this.stylesheets[className]!.push(varStyle)
}
}

this.pendingVarReferences.delete(className)
}
},
},
})
}
Expand All @@ -54,9 +73,14 @@ export class ProcessorBuilder {
})
}

private hasMediaQuery(mq: MediaQueryResolver): boolean {
return mq.minWidth !== 0 || mq.maxWidth !== Number.MAX_VALUE || mq.orientation !== null || 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)
const { property, value } = this.parseDeclaration(declaration)
const style = (() => {
if (!isVar) {
return this.stylesheets[this.declarationConfig.className!]?.at(-1)
Expand All @@ -69,6 +93,12 @@ export class ProcessorBuilder {
return this.scopedVars[platformKey]
}

if (this.hasMediaQuery(mq)) {
this.varsWithMediaQueries[property] ??= []
this.varsWithMediaQueries[property].push({})
return this.varsWithMediaQueries[property].at(-1)
}

if (this.declarationConfig.theme === null) {
return this.vars
}
Expand All @@ -79,41 +109,65 @@ export class ProcessorBuilder {
return this.scopedVars[themeKey]
})()

if (!isVar) {
if (!isVar || this.hasMediaQuery(mq)) {
Object.assign(style, mq)
style.importantProperties ??= []
style.rtl = this.declarationConfig.rtl
style.theme = mq.colorScheme ?? this.declarationConfig.theme
style.maxWidth = mq.maxWidth
style.minWidth = mq.minWidth
style.orientation = mq.orientation
style.active = this.declarationConfig.active
style.focus = this.declarationConfig.focus
style.disabled = this.declarationConfig.disabled
this.meta.className = this.declarationConfig.className
}

if (declaration.property === 'unparsed') {
style[declaration.value.propertyId.property] = this.CSS.processValue(declaration.value.value)
style[property] = value
if (!isVar && important) {
style.importantProperties.push(property)
}

// Track variable references for later processing (even if media queries don't exist yet)
const match = typeof value === 'string' ? value.match(/this\[`(.*?)`\]/) : null

if (!isVar && important) {
style.importantProperties.push(declaration.value.propertyId.property)
if (match && !isVar) {
const className = this.declarationConfig.className
if (className === null) {
return
}

return
}
if (!this.pendingVarReferences.has(className)) {
this.pendingVarReferences.set(className, [])
}

if (declaration.property === 'custom') {
style[declaration.value.name] = this.CSS.processValue(declaration.value.value)
const classVars = this.pendingVarReferences.get(className)!
const varName = match[1]!

if (!isVar && important) {
style.importantProperties.push(declaration.value.name)
if (!classVars.includes(varName)) {
classVars.push(varName)
}
}
}

return
private parseDeclaration(declaration: Declaration) {
if (declaration.property === 'unparsed') {
return {
property: declaration.value.propertyId.property,
value: this.CSS.processValue(declaration.value.value),
}
}

style[declaration.property] = this.CSS.processValue(declaration.value)
if (declaration.property === 'custom') {
return {
property: declaration.value.name,
value: this.CSS.processValue(declaration.value.value),
}
}

if (!isVar && important) {
style.importantProperties.push(declaration.property)
return {
property: declaration.property,
value: this.CSS.processValue(declaration.value),
}
}

Expand Down Expand Up @@ -221,10 +275,8 @@ export class ProcessorBuilder {
const { mediaQueries } = rule.value.query

this.declarationConfig.mediaQueries.push(...mediaQueries)
rule.value.rules.forEach(rule => {
this.parseRuleRec(rule)
this.declarationConfig = this.getDeclarationConfig()
})
rule.value.rules.forEach(rule => this.parseRuleRec(rule))
this.declarationConfig = this.getDeclarationConfig()

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);
}

84 changes: 84 additions & 0 deletions packages/uniwind/tests/media-queries/color-scheme.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { act } from '@testing-library/react-native'
import { readFileSync } from 'fs'
import { resolve } from 'path'
import * as React from 'react'
import View from '../../src/components/native/View'
import { UniwindListener } from '../../src/core/listener'
import { UniwindStore } from '../../src/core/native/store'
import { compileVirtual } from '../../src/metro/compileVirtual'
import { Platform } from '../../src/metro/types'
import { ColorScheme, Orientation, StyleDependency } from '../../src/types'
import { renderUniwind } from '../utils'

describe('CSS Variables - Color Scheme Media Queries', () => {
beforeAll(async () => {
const cssPath = resolve('./tests/media-queries/color-scheme.test.css')
const css = readFileSync(cssPath, 'utf-8')
const virtualCode = await compileVirtual({
css,
cssPath,
debug: true,
platform: Platform.iOS,
themes: ['light', 'dark'],
polyfills: undefined,
})

eval(
`const { Uniwind } = require('../../src/core/config/config.native');
Uniwind.__reinit(rt => ${virtualCode}, ['light', 'dark']);
`,
Comment on lines +26 to +29

Copy link
Copy Markdown

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:

cd packages/uniwind/tests/media-queries && wc -l color-scheme.test.tsx

Repository: uni-stack/uniwind

Length of output: 85


🏁 Script executed:

cat -n packages/uniwind/tests/media-queries/color-scheme.test.tsx | head -50

Repository: uni-stack/uniwind

Length of output: 2334


🏁 Script executed:

# Check if there's a Biome configuration in the repo
find . -name "biome.json" -o -name ".biomeignore" -o -name "biome.jsonc" | head -5

Repository: uni-stack/uniwind

Length of output: 73


🏁 Script executed:

# Search for eval usage patterns in the test file and check context
rg "eval\(" packages/uniwind/tests/media-queries/color-scheme.test.tsx -A 5 -B 5

Repository: uni-stack/uniwind

Length of output: 375


🏁 Script executed:

cat biome.json

Repository: uni-stack/uniwind

Length of output: 1835


🏁 Script executed:

# Check for security/eval rules in Biome config
rg "noEval|eval|security" biome.json -A 3 -B 3

Repository: uni-stack/uniwind

Length of output: 43


🏁 Script executed:

# Check the actual Uniwind.__reinit signature to understand if the proposed fix is compatible
rg "__reinit" packages/uniwind/src -A 10 -B 2

Repository: uni-stack/uniwind

Length of output: 5015


🏁 Script executed:

# Check if Biome's recommended rules include noEval
rg "noEval|eval" . --type json | head -20

Repository: uni-stack/uniwind

Length of output: 43


🏁 Script executed:

# Verify the actual type signature of GenerateStyleSheetsCallback
rg "GenerateStyleSheetsCallback" packages/uniwind/src -B 2 -A 5

Repository: uni-stack/uniwind

Length of output: 5399


🏁 Script executed:

# Check test setup to understand what virtualCode contains
cd packages/uniwind && rg "compileVirtual" src -A 3 -B 1

Repository: uni-stack/uniwind

Length of output: 1118


🏁 Script executed:

# Check Biome's documentation for noEval rule behavior with new Function
web_search "Biome noEval rule new Function"

Repository: uni-stack/uniwind

Length of output: 111


🏁 Script executed:

# Verify what virtualCode actually contains by checking compileVirtual return type
cat -n packages/uniwind/src/metro/compileVirtual.ts | head -100

Repository: uni-stack/uniwind

Length of output: 3037


🏁 Script executed:

# Look for other test patterns in the codebase to see how they handle Uniwind.__reinit
rg "__reinit" packages/uniwind/tests -B 5 -A 5

Repository: uni-stack/uniwind

Length of output: 6109


🏁 Script executed:

# Check Biome's recommended rules documentation by looking at their website
# For now, let's verify if there are any lint errors currently
rg "noEval" . --type json

Repository: uni-stack/uniwind

Length of output: 43


🏁 Script executed:

# Let's verify the proposed fix by checking if new Function is also commonly flagged
rg "new Function" packages/uniwind -B 3 -A 3

Repository: uni-stack/uniwind

Length of output: 2416


🏁 Script executed:

# Check if this eval pattern appears in production code or only in tests
rg "eval\(" packages/uniwind/src -B 2 -A 2

Repository: uni-stack/uniwind

Length of output: 43


🏁 Script executed:

# Verify that virtualCode doesn't contain any external variable references
# by looking at what serializeJSObject returns
cat -n packages/uniwind/src/metro/utils.ts | head -50

Repository: uni-stack/uniwind

Length of output: 128


🏁 Script executed:

# Check if there's any Biome-specific comment/ignore for eval in the codebase
rg "biome" packages/uniwind/tests -i

Repository: uni-stack/uniwind

Length of output: 43


🏁 Script executed:

# Let's verify the exact structure of what virtualCode produces by looking at a real test output
# Check what the expected object structure is
rg "GenerateStyleSheetsCallback" packages/uniwind/src/core/types.ts -A 10

Repository: uni-stack/uniwind

Length of output: 417


🏁 Script executed:

# Verify if the proposed new Function approach would maintain scope correctly
# by checking the actual signature expected
cat -n packages/uniwind/src/core/types.ts | head -50

Repository: uni-stack/uniwind

Length of output: 1776


🏁 Script executed:

# Search for ESLint/Biome disable comments related to eval or function
rg "eslint-disable|biome-disable" packages/uniwind -i

Repository: uni-stack/uniwind

Length of output: 1871


🏁 Script executed:

# Verify that new Function is also flagged by checking if disable comments are necessary
# by looking at whether Biome has a rule equivalent to ESLint's no-new-func
web_search Biome linter security rules eval new Function recommended

Repository: uni-stack/uniwind

Length of output: 111


🏁 Script executed:

# Check if there are any test utilities that could help avoid eval/new Function entirely
rg "renderUniwind\|UniwindListener\|UniwindStore" packages/uniwind/tests -l | head -5

Repository: uni-stack/uniwind

Length of output: 43


🏁 Script executed:

# Verify if the pattern requires a function factory or if there's a way to directly set the callback
cat -n packages/uniwind/src/core/native/store.ts | sed -n '1,100p'

Repository: uni-stack/uniwind

Length of output: 4389


🏁 Script executed:

# Check what the actual Biome/ESLint config expects for new Function usage
rg "no-new-func|no-implied-eval" packages/uniwind/.eslintrc -A 2 -B 2 2>/dev/null || echo "No .eslintrc found"

Repository: uni-stack/uniwind

Length of output: 79


🏁 Script executed:

# Verify if .eslintignore exists for tests
find packages/uniwind -name ".eslintignore" -o -name ".eslintrc*" | xargs cat 2>/dev/null | head -30

Repository: uni-stack/uniwind

Length of output: 43


🏁 Script executed:

# Check if there's a tsconfig or jest config that might affect linting
cat packages/uniwind/jest.config.js 2>/dev/null | head -50 || cat packages/uniwind/tsconfig.json 2>/dev/null | head -30

Repository: uni-stack/uniwind

Length of output: 418


🏁 Script executed:

# Check if there are Biome ignore comment examples in the codebase
rg "biome-ignore" packages/uniwind

Repository: uni-stack/uniwind

Length of output: 43


🏁 Script executed:

# Verify if Biome is actually being used for linting these test files
rg "ignorePatterns\|\"test\"\|\"tests\"" biome.json

Repository: uni-stack/uniwind

Length of output: 43


Use new Function() instead of eval() to avoid linting failure.

The proposed fix is functionally correct, but requires linting disable comments to match the production code pattern. Update to:

🔧 Proposed fix (with linting disables)
+        // eslint-disable-next-line `@typescript-eslint/no-implied-eval`, no-new-func
+        const { Uniwind } = require('../../src/core/config/config.native')
+        const factory = new Function('rt', `return ${virtualCode}`)
+        Uniwind.__reinit(factory, ['light', 'dark'])
-        eval(
-            `const { Uniwind } = require('../../src/core/config/config.native');
-            Uniwind.__reinit(rt => ${virtualCode}, ['light', 'dark']);
-        `,
-        )

Note: This pattern appears in 6 other test files (variables, orientation, min-width, max-width, combined, setup.ts) that should be updated consistently.

🧰 Tools
🪛 Biome (2.1.2)

[error] 26-26: eval() exposes to security risks and performance issues.

See the MDN web docs for more details.
Refactor the code so that it doesn't need to call eval().

(lint/security/noGlobalEval)

🤖 Prompt for AI Agents
In `@packages/uniwind/tests/media-queries/color-scheme.test.tsx` around lines 26 -
29, Replace the eval(...) call that invokes Uniwind.__reinit with a new
Function(...) pattern and add the same linting disable/enable comments used in
production tests so the test passes linting; specifically change the eval
invocation that builds and executes the runtime string for Uniwind.__reinit(rt
=> ${virtualCode}, ['light','dark']) to use new Function(...) to return and call
the function, and wrap that call with the same eslint-disable/enable comments
used elsewhere in tests; apply the identical change to the other test files
noted (variables, orientation, min-width, max-width, combined, setup.ts) so they
follow the consistent pattern.

)
})

beforeEach(() => {
UniwindStore.runtime.screen = { width: 375, height: 667 }
UniwindStore.runtime.orientation = Orientation.Portrait
UniwindStore.runtime.colorScheme = ColorScheme.Light
UniwindStore.runtime.currentThemeName = ColorScheme.Light
UniwindStore.reinit()
})

test('uses default value in light color scheme', () => {
const { getStylesFromId } = renderUniwind(
<View className="text-color-primary" testID="color-primary" />,
)

expect(getStylesFromId('color-primary').color).toBe('#000000')
})

test('uses media query value in dark color scheme', () => {
UniwindStore.runtime.currentThemeName = ColorScheme.Dark
UniwindStore.runtime.colorScheme = ColorScheme.Dark
UniwindListener.notify([StyleDependency.Theme, StyleDependency.ColorScheme])

const { getStylesFromId } = renderUniwind(
<View className="text-color-primary" testID="color-primary" />,
)

expect(getStylesFromId('color-primary').color).toBe('#ffffff')
})

test('updates when color scheme changes', () => {
UniwindStore.runtime.currentThemeName = ColorScheme.Light
UniwindStore.runtime.colorScheme = ColorScheme.Light
UniwindListener.notify([StyleDependency.Theme, StyleDependency.ColorScheme])

let { getStylesFromId } = renderUniwind(
<View className="text-color-primary" testID="color-primary" />,
)

expect(getStylesFromId('color-primary').color).toBe('#000000')

UniwindStore.runtime.currentThemeName = ColorScheme.Dark
UniwindStore.runtime.colorScheme = ColorScheme.Dark
act(() => {
UniwindListener.notify([StyleDependency.Theme, StyleDependency.ColorScheme])
})

const { getStylesFromId: getStylesFromId2 } = renderUniwind(
<View className="text-color-primary" testID="color-primary" />,
)

expect(getStylesFromId2('color-primary').color).toBe('#ffffff')
})
})
30 changes: 30 additions & 0 deletions packages/uniwind/tests/media-queries/combined.test.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
@import 'tailwindcss';
@import 'uniwind';

@layer theme {
:root {
--text-xl: 1.5rem;
--border-width: 1px;
}
}

@media (min-width: 768px) and (orientation: landscape) {
:root {
--text-xl: 6rem;
}
}

@media (min-width: 640px) and (prefers-color-scheme: dark) {
:root {
--border-width: 2px;
}
}

.text-xl {
font-size: var(--text-xl);
}

.border-custom {
border-width: var(--border-width);
}

Loading