From 0e7f3118364dbb2c05bf4b85b3de1f9eb076f16a Mon Sep 17 00:00:00 2001 From: Eliot Gevers Date: Sun, 4 Oct 2026 01:25:07 +0200 Subject: [PATCH 1/3] fix: reset root parser state before sibling rules --- .../src/bundler/css-processor/processor.ts | 1 + .../native/styles-parsing/root-state.test.ts | 40 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 packages/uniwind/tests/native/styles-parsing/root-state.test.ts diff --git a/packages/uniwind/src/bundler/css-processor/processor.ts b/packages/uniwind/src/bundler/css-processor/processor.ts index e77c2a05..e9fc0759 100644 --- a/packages/uniwind/src/bundler/css-processor/processor.ts +++ b/packages/uniwind/src/bundler/css-processor/processor.ts @@ -295,6 +295,7 @@ export class ProcessorBuilder { rule.value.declarations?.declarations?.forEach(declaration => this.addDeclaration(declaration)) rule.value.declarations?.importantDeclarations?.forEach(declaration => this.addDeclaration(declaration, true)) rule.value.rules?.forEach(rule => this.parseRuleRec(rule)) + this.declarationConfig.root = false } }) }) diff --git a/packages/uniwind/tests/native/styles-parsing/root-state.test.ts b/packages/uniwind/tests/native/styles-parsing/root-state.test.ts new file mode 100644 index 00000000..9525062a --- /dev/null +++ b/packages/uniwind/tests/native/styles-parsing/root-state.test.ts @@ -0,0 +1,40 @@ +import { UniwindBundlerConfig } from '../../../src/bundler/config' +import { ProcessorBuilder } from '../../../src/bundler/css-processor' +import { Platform } from '../../../src/common/consts' + +const compile = (css: string) => { + const config = UniwindBundlerConfig.fromMetroConfig({ cssEntryFile: './tests/test.css' }, Platform.iOS) + const processor = new ProcessorBuilder(config) + + processor.transform(css) + + return processor +} + +const rules = ':root { --root-width: 10px; } .after-root { width: 42px; }' + +describe(':root parser state', () => { + test.each([ + ['top level', rules], + ['layer', `@layer utilities { ${rules} }`], + ['supports', `@supports (display: grid) { ${rules} }`], + ])('does not leak into the next class at %s', (_name, css) => { + const processor = compile(css) + + expect(processor.vars['--root-width']).toBeDefined() + expect(processor.vars.width).toBeUndefined() + expect(processor.stylesheets['after-root'][0].width).toBe(42) + }) + + test.each([ + ['layer', `@layer utilities { ${rules} }`], + ['supports', `@supports (display: grid) { ${rules} }`], + ])('preserves the media condition around a %s', (_name, css) => { + const processor = compile(`@media ios { ${css} }`) + const style = processor.stylesheets['after-root'][0] + + expect(style.width).toBe(42) + expect(style.platform).toBe(Platform.iOS) + expect(processor.vars.width).toBeUndefined() + }) +}) From e16e5906bc386850d13dfbafc5ea5a2e746dc7f0 Mon Sep 17 00:00:00 2001 From: Eliot Gevers Date: Sun, 4 Oct 2026 12:25:42 +0200 Subject: [PATCH 2/3] fix: preserve outer root state during nested parsing --- .../src/bundler/css-processor/processor.ts | 3 ++- .../native/styles-parsing/root-state.test.ts | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/uniwind/src/bundler/css-processor/processor.ts b/packages/uniwind/src/bundler/css-processor/processor.ts index e9fc0759..1c59170b 100644 --- a/packages/uniwind/src/bundler/css-processor/processor.ts +++ b/packages/uniwind/src/bundler/css-processor/processor.ts @@ -290,12 +290,13 @@ export class ProcessorBuilder { selector.forEach(selectorToken => { if (selectorToken.type === 'pseudo-class' && selectorToken.kind === 'root') { + const previousRoot = this.declarationConfig.root this.declarationConfig.root = true rule.value.declarations?.declarations?.forEach(declaration => this.addDeclaration(declaration)) rule.value.declarations?.importantDeclarations?.forEach(declaration => this.addDeclaration(declaration, true)) rule.value.rules?.forEach(rule => this.parseRuleRec(rule)) - this.declarationConfig.root = false + this.declarationConfig.root = previousRoot } }) }) diff --git a/packages/uniwind/tests/native/styles-parsing/root-state.test.ts b/packages/uniwind/tests/native/styles-parsing/root-state.test.ts index 9525062a..2a72a8b3 100644 --- a/packages/uniwind/tests/native/styles-parsing/root-state.test.ts +++ b/packages/uniwind/tests/native/styles-parsing/root-state.test.ts @@ -14,6 +14,21 @@ const compile = (css: string) => { const rules = ':root { --root-width: 10px; } .after-root { width: 42px; }' describe(':root parser state', () => { + test('preserves the outer root state after a nested root rule', () => { + const processor = compile(` + .box, :root { + &:root { --inner: 1px; } + --after: 2px; + } + .after-root { width: 42px; } + `) + + expect(processor.vars['--inner']).toBeDefined() + expect(processor.vars['--after']).toBeDefined() + expect(processor.vars.width).toBeUndefined() + expect(processor.stylesheets['after-root'][0].width).toBe(42) + }) + test.each([ ['top level', rules], ['layer', `@layer utilities { ${rules} }`], From 9de16d712bae3b53c40cbde2b25536e8ec929849 Mon Sep 17 00:00:00 2001 From: Eliot Gevers Date: Sun, 4 Oct 2026 13:07:17 +0200 Subject: [PATCH 3/3] fix: scope root state to each style rule --- .../src/bundler/css-processor/processor.ts | 6 ++- .../native/styles-parsing/root-state.test.ts | 37 ++++++++++++++++++- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/packages/uniwind/src/bundler/css-processor/processor.ts b/packages/uniwind/src/bundler/css-processor/processor.ts index 1c59170b..4e290ac7 100644 --- a/packages/uniwind/src/bundler/css-processor/processor.ts +++ b/packages/uniwind/src/bundler/css-processor/processor.ts @@ -240,9 +240,12 @@ export class ProcessorBuilder { } if (rule.type === 'style') { + const previousRoot = this.declarationConfig.root + rule.value.selectors.forEach(selector => { const [maybeClassNameSelector] = selector const newClassName = maybeClassNameSelector?.type === 'class' ? maybeClassNameSelector.name : undefined + this.declarationConfig.root = newClassName === undefined ? previousRoot : false if (newClassName !== undefined) { this.declarationConfig.className = newClassName @@ -290,16 +293,15 @@ export class ProcessorBuilder { selector.forEach(selectorToken => { if (selectorToken.type === 'pseudo-class' && selectorToken.kind === 'root') { - const previousRoot = this.declarationConfig.root this.declarationConfig.root = true rule.value.declarations?.declarations?.forEach(declaration => this.addDeclaration(declaration)) rule.value.declarations?.importantDeclarations?.forEach(declaration => this.addDeclaration(declaration, true)) rule.value.rules?.forEach(rule => this.parseRuleRec(rule)) - this.declarationConfig.root = previousRoot } }) }) + this.declarationConfig.root = previousRoot return } diff --git a/packages/uniwind/tests/native/styles-parsing/root-state.test.ts b/packages/uniwind/tests/native/styles-parsing/root-state.test.ts index 2a72a8b3..b15d2549 100644 --- a/packages/uniwind/tests/native/styles-parsing/root-state.test.ts +++ b/packages/uniwind/tests/native/styles-parsing/root-state.test.ts @@ -14,9 +14,42 @@ const compile = (css: string) => { const rules = ':root { --root-width: 10px; } .after-root { width: 42px; }' describe(':root parser state', () => { - test('preserves the outer root state after a nested root rule', () => { + test.each([ + ['plain', '.sibling&', false], + ['active', '.sibling&:active', true], + ])('keeps nested %s class declarations out of root variables', (_name, selector, active) => { + const processor = compile(` + :root { + &:root { --inner: 1px; } + ${selector} { width: 5px !important; } + } + `) + + expect(processor.stylesheets.sibling[0].width).toBe(5) + expect(processor.stylesheets.sibling[0].active).toBe(active || null) + expect(processor.stylesheets.sibling[0].importantProperties).toContain('width') + expect(processor.vars.width).toBeUndefined() + expect(processor.vars['--inner']).toBeDefined() + }) + + test('preserves root declarations after nested class and root rules', () => { + const processor = compile(` + :root { + .before& { height: 3px; } + &:root { --inner: 1px; } + --after: 2px; + } + `) + + expect(processor.vars['--after']).toBeDefined() + expect(processor.stylesheets.before[0].height).toBe(3) + expect(processor.vars.height).toBeUndefined() + expect(processor.stylesheets.before.some(style => '--after' in style)).toBe(false) + }) + + test.each(['.box, :root', ':root, .box'])('preserves the outer root state in %s', selector => { const processor = compile(` - .box, :root { + ${selector} { &:root { --inner: 1px; } --after: 2px; }