authorgravatar for 48803618+Brentlok@users.noreply.github.comHubert Bieszczad <48803618+Brentlok@users.noreply.github.com> 2026-10-05 07:57:04+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-05 05:57:04+00:00
log250e052f44dd313b658abebc707242a7b25be340
treeb1d5377f180cae6db170cdf2363cebc44ea10488
parent2a778f7a6e7b5f9403c5dbff2c2f4a9ce26c0ade
signature Signed by PGP key B5690EEEBB952194

fix(mobile): upgrade Uniwind and remove local patch (#14597)


6 files changed, 16 insertions(+), 1031 deletions(-)

apps/mobile/package.json+1-1
...@@ -127,7 +127,7 @@...@@ -127,7 +127,7 @@
127 "react-native-worklets": "0.13.0",127 "react-native-worklets": "0.13.0",
128 "shiki": "4.2.0",128 "shiki": "4.2.0",
129 "tailwind-merge": "^3.5.0",129 "tailwind-merge": "^3.5.0",
130 "uniwind": "1.11.0"130 "uniwind": "1.12.1"
131 },131 },
132 "devDependencies": {132 "devDependencies": {
133 "@effect/vitest": "catalog:",133 "@effect/vitest": "catalog:",
apps/mobile/src/lib/uniwind-platform-variants.test.ts+4-6
...@@ -10,7 +10,7 @@ import { beforeAll, describe, expect, it } from "vite-plus/test";...@@ -10,7 +10,7 @@ import { beforeAll, describe, expect, it } from "vite-plus/test";
10// `@media ios { ... }` block, and uniwind's CSS processor used to drop the10// `@media ios { ... }` block, and uniwind's CSS processor used to drop the
11// block's media queries after its first nested rule. Everything past the first11// block's media queries after its first nested rule. Everything past the first
12// utility compiled unguarded and shipped to both platforms. These tests run12// utility compiled unguarded and shipped to both platforms. These tests run
13// the installed (patched) uniwind compiler over real Tailwind output, per13// the installed uniwind compiler over real Tailwind output, per
14// platform, and assert what each bundle receives. The compiler itself runs in14// platform, and assert what each bundle receives. The compiler itself runs in
15// a plain Node child process (uniwind-platform-variants.fixture.cjs) so no15// a plain Node child process (uniwind-platform-variants.fixture.cjs) so no
16// test-runner module transforms sit between the test and the shipped code.16// test-runner module transforms sit between the test and the shipped code.
...@@ -215,11 +215,9 @@ describe("uniwind keeps media rules nested inside class rules attached to the cl...@@ -215,11 +215,9 @@ describe("uniwind keeps media rules nested inside class rules attached to the cl
215 });215 });
216});216});
217217
218// The patch file carries three independent uniwind fixes (state/data selector218// Keep guarding selector variants and the Metro native-styles fingerprint
219// variants, the Metro native-styles fingerprint, and this media-query scoping219// alongside media-query scoping when upgrading the installed Uniwind package.
220// fix). Regenerating it for one fix must not silently drop the others — this220describe("uniwind keeps selector and transformer behavior", () => {
221// suite exercises the shipped transformer and compiler for the other two.
222describe("uniwind patch keeps pre-existing selector and transformer behavior", () => {
223 let output: FixtureOutput;221 let output: FixtureOutput;
224222
225 beforeAll(() => {223 beforeAll(() => {
docs/internals/mobile-development.md+5-6
...@@ -17,12 +17,11 @@ shutdown, but a supervisor created after its cleanup runs would escape it. A clo...@@ -17,12 +17,11 @@ shutdown, but a supervisor created after its cleanup runs would escape it. A clo
17parent scope also closes late arrivals, preventing interrupted startup or runtime17parent scope also closes late arrivals, preventing interrupted startup or runtime
18replacement from leaving a WebSocket alive outside the new registry.18replacement from leaving a WebSocket alive outside the new registry.
1919
20The [Uniwind patch](../../patches/uniwind@1.11.0.patch) still compiles CSS on Metro20Uniwind compiles CSS on Metro updates so newly used classes are discovered. It
21updates so newly used classes are discovered. It skips global style invalidation21skips global style invalidation only when the generated stylesheet and theme list
22only when the generated stylesheet and theme list are unchanged. Skipping compilation22are unchanged. Skipping compilation would lose new classes; invalidating every
23would lose new classes; invalidating every consumer for unchanged output makes an23consumer for unchanged output makes an ordinary component edit refresh the whole
24ordinary component edit refresh the whole app. The fingerprint is recorded only24app. The fingerprint is recorded only after initialization succeeds.
25after initialization succeeds.
2625
27The [expo-notifications patch](../../patches/expo-notifications@57.0.15.patch) protects26The [expo-notifications patch](../../patches/expo-notifications@57.0.15.patch) protects
28`NotificationCenterManager`'s delegates and pending responses with a lock. React runtimes can27`NotificationCenterManager`'s delegates and pending responses with a lock. React runtimes can
patches/uniwind@1.11.0.patch deleted-1011
...@@ -1,1011 +0,0 @@
1diff --git a/dist/common/bundler/css-processor/processor.js b/dist/common/bundler/css-processor/processor.js
2index 7dd369f..48321d1 100644
3--- a/dist/common/bundler/css-processor/processor.js
4+++ b/dist/common/bundler/css-processor/processor.js
5@@ -105,6 +105,64 @@ class ProcessorBuilder {
6 style.importantProperties.push(declaration.property);
7 }
8 }
9+ readSelectorVariants(tokens) {
10+ const variants = {
11+ rtl: null,
12+ theme: null,
13+ active: null,
14+ focus: null,
15+ disabled: null,
16+ dataAttributes: null
17+ };
18+ tokens.forEach((token) => {
19+ if (token.type === "pseudo-class" && token.kind === "where") {
20+ token.selectors.forEach((compound) => {
21+ compound.forEach((inner) => {
22+ if (inner.type === "class" && this.bundlerConfig.themes.includes(inner.name)) {
23+ variants.theme = inner.name;
24+ }
25+ if (inner.type === "pseudo-class" && inner.kind === "dir") {
26+ variants.rtl = inner.direction === "rtl";
27+ }
28+ });
29+ });
30+ }
31+ if (token.type === "pseudo-class" && token.kind === "active") {
32+ variants.active = true;
33+ }
34+ if (token.type === "pseudo-class" && token.kind === "focus") {
35+ variants.focus = true;
36+ }
37+ if (token.type === "pseudo-class" && token.kind === "disabled") {
38+ variants.disabled = true;
39+ }
40+ if (token.type === "attribute" && token.operation === null && token.name.startsWith("data-")) {
41+ variants.dataAttributes ??= {};
42+ variants.dataAttributes[token.name] = `"true"`;
43+ }
44+ if (token.type === "attribute" && token.operation?.operator === "equal" && token.name.startsWith("data-")) {
45+ variants.dataAttributes ??= {};
46+ variants.dataAttributes[token.name] = `"${token.operation.value}"`;
47+ }
48+ });
49+ const hasVariants = Object.values(variants).some(_utils.isDefined);
50+ return { variants, hasVariants };
51+ }
52+ withSelectorVariants(variants, run) {
53+ this.declarationConfig.rtl ??= variants.rtl;
54+ this.declarationConfig.theme ??= variants.theme;
55+ this.declarationConfig.active ??= variants.active;
56+ this.declarationConfig.focus ??= variants.focus;
57+ this.declarationConfig.disabled ??= variants.disabled;
58+ this.declarationConfig.dataAttributes ??= variants.dataAttributes;
59+ run();
60+ this.declarationConfig.rtl = null;
61+ this.declarationConfig.theme = null;
62+ this.declarationConfig.active = null;
63+ this.declarationConfig.focus = null;
64+ this.declarationConfig.disabled = null;
65+ this.declarationConfig.dataAttributes = null;
66+ }
67 parseRuleRec(rule) {
68 if (this.declarationConfig.className !== null) {
69 const lastStyle = this.stylesheets[this.declarationConfig.className]?.at(-1);
70@@ -120,64 +178,30 @@ class ProcessorBuilder {
71 this.declarationConfig.className = newClassName;
72 this.stylesheets[newClassName] ??= [];
73 this.stylesheets[newClassName].push({});
74- rule.value.declarations?.declarations?.forEach(declaration => this.addDeclaration(declaration));
75- rule.value.declarations?.importantDeclarations?.forEach(declaration => this.addDeclaration(declaration, true));
76- rule.value.rules?.forEach(rule2 => this.parseRuleRec(rule2));
77- return;
78- }
79- let rtl = null;
80- let theme = null;
81- let active = null;
82- let focus = null;
83- let disabled = null;
84- let dataAttributes = null;
85- selector.forEach(selector2 => {
86- if (selector2.type === "pseudo-class" && selector2.kind === "where") {
87- selector2.selectors.forEach(selector3 => {
88- selector3.forEach(selector4 => {
89- if (selector4.type === "class" && this.bundlerConfig.themes.includes(selector4.name)) {
90- theme = selector4.name;
91- }
92- if (selector4.type === "pseudo-class" && selector4.kind === "dir") {
93- rtl = selector4.direction === "rtl";
94- }
95- });
96- });
97- }
98- if (selector2.type === "pseudo-class" && selector2.kind === "active") {
99- active = true;
100+ const trailing = selector.slice(1);
101+ const { variants, hasVariants } = this.readSelectorVariants(trailing);
102+ if (trailing.length > 0 && !hasVariants) {
103+ return;
104 }
105- if (selector2.type === "pseudo-class" && selector2.kind === "focus") {
106- focus = true;
107- }
108- if (selector2.type === "pseudo-class" && selector2.kind === "disabled") {
109- disabled = true;
110- }
111- if (selector2.type === "attribute" && selector2.operation === null && selector2.name.startsWith("data-")) {
112- dataAttributes ??= {};
113- dataAttributes[selector2.name] = `"true"`;
114- }
115- if (selector2.type === "attribute" && selector2.operation?.operator === "equal" && selector2.name.startsWith("data-")) {
116- dataAttributes ??= {};
117- dataAttributes[selector2.name] = `"${selector2.operation.value}"`;
118+ const parseClassRule = () => {
119+ rule.value.declarations?.declarations?.forEach(declaration => this.addDeclaration(declaration));
120+ rule.value.declarations?.importantDeclarations?.forEach(declaration => this.addDeclaration(declaration, true));
121+ rule.value.rules?.forEach(rule2 => this.parseRuleRec(rule2));
122+ };
123+ if (hasVariants) {
124+ this.withSelectorVariants(variants, parseClassRule);
125+ } else {
126+ parseClassRule();
127 }
128- });
129- if ([rtl, theme, active, focus, disabled, dataAttributes].some(_utils.isDefined)) {
130- this.declarationConfig.rtl ??= rtl;
131- this.declarationConfig.theme ??= theme;
132- this.declarationConfig.active ??= active;
133- this.declarationConfig.focus ??= focus;
134- this.declarationConfig.disabled ??= disabled;
135- this.declarationConfig.dataAttributes ??= dataAttributes;
136- rule.value.declarations?.declarations?.forEach(declaration => this.addDeclaration(declaration));
137- rule.value.declarations?.importantDeclarations?.forEach(declaration => this.addDeclaration(declaration, true));
138- rule.value.rules?.forEach(rule2 => this.parseRuleRec(rule2));
139- this.declarationConfig.rtl = null;
140- this.declarationConfig.theme = null;
141- this.declarationConfig.active = null;
142- this.declarationConfig.focus = null;
143- this.declarationConfig.disabled = null;
144- this.declarationConfig.dataAttributes = null;
145+ return;
146+ }
147+ const { variants, hasVariants } = this.readSelectorVariants(selector);
148+ if (hasVariants) {
149+ this.withSelectorVariants(variants, () => {
150+ rule.value.declarations?.declarations?.forEach(declaration => this.addDeclaration(declaration));
151+ rule.value.declarations?.importantDeclarations?.forEach(declaration => this.addDeclaration(declaration, true));
152+ rule.value.rules?.forEach(rule2 => this.parseRuleRec(rule2));
153+ });
154 return;
155 }
156 selector.forEach(selectorToken => {
157@@ -199,11 +223,16 @@ class ProcessorBuilder {
158 const {
159 mediaQueries
160 } = rule.value.query;
161- this.declarationConfig.mediaQueries.push(...mediaQueries);
162+ const outerConfig = this.declarationConfig;
163+ const blockMediaQueries = [...outerConfig.mediaQueries, ...mediaQueries];
164 rule.value.rules.forEach(rule2 => {
165+ this.declarationConfig = {
166+ ...outerConfig,
167+ mediaQueries: blockMediaQueries
168+ };
169 this.parseRuleRec(rule2);
170- this.declarationConfig = this.getDeclarationConfig();
171 });
172+ this.declarationConfig = outerConfig;
173 return;
174 }
175 if (rule.type === "layer-block") {
176diff --git a/dist/metro/transformer.cjs b/dist/metro/transformer.cjs
177index d57ca81..83b58d1 100644
178--- a/dist/metro/transformer.cjs
179+++ b/dist/metro/transformer.cjs
180@@ -7,6 +7,7 @@ const culori = require('culori');
181 const node = require('@tailwindcss/node');
182 const oxide = require('@tailwindcss/oxide');
183 const fs = require('fs');
184+const node_crypto = require('node:crypto');
185
186 function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
187
188@@ -1537,6 +1538,64 @@ class ProcessorBuilder {
189 style.importantProperties.push(declaration.property);
190 }
191 }
192+ readSelectorVariants(tokens) {
193+ const variants = {
194+ rtl: null,
195+ theme: null,
196+ active: null,
197+ focus: null,
198+ disabled: null,
199+ dataAttributes: null
200+ };
201+ tokens.forEach((token) => {
202+ if (token.type === "pseudo-class" && token.kind === "where") {
203+ token.selectors.forEach((compound) => {
204+ compound.forEach((inner) => {
205+ if (inner.type === "class" && this.bundlerConfig.themes.includes(inner.name)) {
206+ variants.theme = inner.name;
207+ }
208+ if (inner.type === "pseudo-class" && inner.kind === "dir") {
209+ variants.rtl = inner.direction === "rtl";
210+ }
211+ });
212+ });
213+ }
214+ if (token.type === "pseudo-class" && token.kind === "active") {
215+ variants.active = true;
216+ }
217+ if (token.type === "pseudo-class" && token.kind === "focus") {
218+ variants.focus = true;
219+ }
220+ if (token.type === "pseudo-class" && token.kind === "disabled") {
221+ variants.disabled = true;
222+ }
223+ if (token.type === "attribute" && token.operation === null && token.name.startsWith("data-")) {
224+ variants.dataAttributes ??= {};
225+ variants.dataAttributes[token.name] = `"true"`;
226+ }
227+ if (token.type === "attribute" && token.operation?.operator === "equal" && token.name.startsWith("data-")) {
228+ variants.dataAttributes ??= {};
229+ variants.dataAttributes[token.name] = `"${token.operation.value}"`;
230+ }
231+ });
232+ const hasVariants = Object.values(variants).some(config.isDefined);
233+ return { variants, hasVariants };
234+ }
235+ withSelectorVariants(variants, run) {
236+ this.declarationConfig.rtl ??= variants.rtl;
237+ this.declarationConfig.theme ??= variants.theme;
238+ this.declarationConfig.active ??= variants.active;
239+ this.declarationConfig.focus ??= variants.focus;
240+ this.declarationConfig.disabled ??= variants.disabled;
241+ this.declarationConfig.dataAttributes ??= variants.dataAttributes;
242+ run();
243+ this.declarationConfig.rtl = null;
244+ this.declarationConfig.theme = null;
245+ this.declarationConfig.active = null;
246+ this.declarationConfig.focus = null;
247+ this.declarationConfig.disabled = null;
248+ this.declarationConfig.dataAttributes = null;
249+ }
250 parseRuleRec(rule) {
251 if (this.declarationConfig.className !== null) {
252 const lastStyle = this.stylesheets[this.declarationConfig.className]?.at(-1);
253@@ -1552,64 +1611,30 @@ class ProcessorBuilder {
254 this.declarationConfig.className = newClassName;
255 this.stylesheets[newClassName] ??= [];
256 this.stylesheets[newClassName].push({});
257- rule.value.declarations?.declarations?.forEach((declaration) => this.addDeclaration(declaration));
258- rule.value.declarations?.importantDeclarations?.forEach((declaration) => this.addDeclaration(declaration, true));
259- rule.value.rules?.forEach((rule2) => this.parseRuleRec(rule2));
260- return;
261- }
262- let rtl = null;
263- let theme = null;
264- let active = null;
265- let focus = null;
266- let disabled = null;
267- let dataAttributes = null;
268- selector.forEach((selector2) => {
269- if (selector2.type === "pseudo-class" && selector2.kind === "where") {
270- selector2.selectors.forEach((selector3) => {
271- selector3.forEach((selector4) => {
272- if (selector4.type === "class" && this.bundlerConfig.themes.includes(selector4.name)) {
273- theme = selector4.name;
274- }
275- if (selector4.type === "pseudo-class" && selector4.kind === "dir") {
276- rtl = selector4.direction === "rtl";
277- }
278- });
279- });
280- }
281- if (selector2.type === "pseudo-class" && selector2.kind === "active") {
282- active = true;
283- }
284- if (selector2.type === "pseudo-class" && selector2.kind === "focus") {
285- focus = true;
286+ const trailing = selector.slice(1);
287+ const { variants, hasVariants } = this.readSelectorVariants(trailing);
288+ if (trailing.length > 0 && !hasVariants) {
289+ return;
290 }
291- if (selector2.type === "pseudo-class" && selector2.kind === "disabled") {
292- disabled = true;
293- }
294- if (selector2.type === "attribute" && selector2.operation === null && selector2.name.startsWith("data-")) {
295- dataAttributes ??= {};
296- dataAttributes[selector2.name] = `"true"`;
297- }
298- if (selector2.type === "attribute" && selector2.operation?.operator === "equal" && selector2.name.startsWith("data-")) {
299- dataAttributes ??= {};
300- dataAttributes[selector2.name] = `"${selector2.operation.value}"`;
301+ const parseClassRule = () => {
302+ rule.value.declarations?.declarations?.forEach((declaration) => this.addDeclaration(declaration));
303+ rule.value.declarations?.importantDeclarations?.forEach((declaration) => this.addDeclaration(declaration, true));
304+ rule.value.rules?.forEach((rule2) => this.parseRuleRec(rule2));
305+ };
306+ if (hasVariants) {
307+ this.withSelectorVariants(variants, parseClassRule);
308+ } else {
309+ parseClassRule();
310 }
311- });
312- if ([rtl, theme, active, focus, disabled, dataAttributes].some(config.isDefined)) {
313- this.declarationConfig.rtl ??= rtl;
314- this.declarationConfig.theme ??= theme;
315- this.declarationConfig.active ??= active;
316- this.declarationConfig.focus ??= focus;
317- this.declarationConfig.disabled ??= disabled;
318- this.declarationConfig.dataAttributes ??= dataAttributes;
319- rule.value.declarations?.declarations?.forEach((declaration) => this.addDeclaration(declaration));
320- rule.value.declarations?.importantDeclarations?.forEach((declaration) => this.addDeclaration(declaration, true));
321- rule.value.rules?.forEach((rule2) => this.parseRuleRec(rule2));
322- this.declarationConfig.rtl = null;
323- this.declarationConfig.theme = null;
324- this.declarationConfig.active = null;
325- this.declarationConfig.focus = null;
326- this.declarationConfig.disabled = null;
327- this.declarationConfig.dataAttributes = null;
328+ return;
329+ }
330+ const { variants, hasVariants } = this.readSelectorVariants(selector);
331+ if (hasVariants) {
332+ this.withSelectorVariants(variants, () => {
333+ rule.value.declarations?.declarations?.forEach((declaration) => this.addDeclaration(declaration));
334+ rule.value.declarations?.importantDeclarations?.forEach((declaration) => this.addDeclaration(declaration, true));
335+ rule.value.rules?.forEach((rule2) => this.parseRuleRec(rule2));
336+ });
337 return;
338 }
339 selector.forEach((selectorToken) => {
340@@ -1629,11 +1654,13 @@ class ProcessorBuilder {
341 }
342 if (rule.type === "media") {
343 const { mediaQueries } = rule.value.query;
344- this.declarationConfig.mediaQueries.push(...mediaQueries);
345+ const outerConfig = this.declarationConfig;
346+ const blockMediaQueries = [...outerConfig.mediaQueries, ...mediaQueries];
347 rule.value.rules.forEach((rule2) => {
348+ this.declarationConfig = { ...outerConfig, mediaQueries: blockMediaQueries };
349 this.parseRuleRec(rule2);
350- this.declarationConfig = this.getDeclarationConfig();
351 });
352+ this.declarationConfig = outerConfig;
353 return;
354 }
355 if (rule.type === "layer-block") {
356@@ -1753,10 +1780,11 @@ const transform = async (config$1, projectRoot, filePath, data, options) => {
357 await bundlerConfig.generateArtifacts(cssArtifactPath);
358 const virtualCode = await compileCSS(bundlerConfig);
359 const isWeb = bundlerConfig.platform === config.Platform.Web;
360+ const nativeStylesFingerprint = isWeb ? void 0 : node_crypto.createHash("sha256").update(virtualCode).update("\0").update(bundlerConfig.stringifiedThemes).digest("hex");
361 data = Buffer.from(
362 isWeb ? virtualCode : [
363 `const { Uniwind } = require('uniwind');`,
364- `Uniwind.__reinit(rt => ${virtualCode}, ${bundlerConfig.stringifiedThemes});`
365+ `Uniwind.__reinit(rt => ${virtualCode}, ${bundlerConfig.stringifiedThemes}, '${nativeStylesFingerprint}');`
366 ].join(""),
367 "utf-8"
368 );
369diff --git a/dist/metro/transformer.mjs b/dist/metro/transformer.mjs
370index d3e7475..f92b73b 100644
371--- a/dist/metro/transformer.mjs
372+++ b/dist/metro/transformer.mjs
373@@ -5,6 +5,7 @@ import { converter, parse, formatHex, formatHex8 } from 'culori';
374 import { compile } from '@tailwindcss/node';
375 import { Scanner } from '@tailwindcss/oxide';
376 import fs from 'fs';
377+import { createHash } from 'node:crypto';
378
379 const toCamelCase = (str) => str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
380 const pipe = (data) => ((...fns) => fns.reduce((acc, fn) => fn(acc), data));
381@@ -1530,6 +1531,64 @@ class ProcessorBuilder {
382 style.importantProperties.push(declaration.property);
383 }
384 }
385+ readSelectorVariants(tokens) {
386+ const variants = {
387+ rtl: null,
388+ theme: null,
389+ active: null,
390+ focus: null,
391+ disabled: null,
392+ dataAttributes: null
393+ };
394+ tokens.forEach((token) => {
395+ if (token.type === "pseudo-class" && token.kind === "where") {
396+ token.selectors.forEach((compound) => {
397+ compound.forEach((inner) => {
398+ if (inner.type === "class" && this.bundlerConfig.themes.includes(inner.name)) {
399+ variants.theme = inner.name;
400+ }
401+ if (inner.type === "pseudo-class" && inner.kind === "dir") {
402+ variants.rtl = inner.direction === "rtl";
403+ }
404+ });
405+ });
406+ }
407+ if (token.type === "pseudo-class" && token.kind === "active") {
408+ variants.active = true;
409+ }
410+ if (token.type === "pseudo-class" && token.kind === "focus") {
411+ variants.focus = true;
412+ }
413+ if (token.type === "pseudo-class" && token.kind === "disabled") {
414+ variants.disabled = true;
415+ }
416+ if (token.type === "attribute" && token.operation === null && token.name.startsWith("data-")) {
417+ variants.dataAttributes ??= {};
418+ variants.dataAttributes[token.name] = `"true"`;
419+ }
420+ if (token.type === "attribute" && token.operation?.operator === "equal" && token.name.startsWith("data-")) {
421+ variants.dataAttributes ??= {};
422+ variants.dataAttributes[token.name] = `"${token.operation.value}"`;
423+ }
424+ });
425+ const hasVariants = Object.values(variants).some(isDefined);
426+ return { variants, hasVariants };
427+ }
428+ withSelectorVariants(variants, run) {
429+ this.declarationConfig.rtl ??= variants.rtl;
430+ this.declarationConfig.theme ??= variants.theme;
431+ this.declarationConfig.active ??= variants.active;
432+ this.declarationConfig.focus ??= variants.focus;
433+ this.declarationConfig.disabled ??= variants.disabled;
434+ this.declarationConfig.dataAttributes ??= variants.dataAttributes;
435+ run();
436+ this.declarationConfig.rtl = null;
437+ this.declarationConfig.theme = null;
438+ this.declarationConfig.active = null;
439+ this.declarationConfig.focus = null;
440+ this.declarationConfig.disabled = null;
441+ this.declarationConfig.dataAttributes = null;
442+ }
443 parseRuleRec(rule) {
444 if (this.declarationConfig.className !== null) {
445 const lastStyle = this.stylesheets[this.declarationConfig.className]?.at(-1);
446@@ -1545,64 +1604,30 @@ class ProcessorBuilder {
447 this.declarationConfig.className = newClassName;
448 this.stylesheets[newClassName] ??= [];
449 this.stylesheets[newClassName].push({});
450- rule.value.declarations?.declarations?.forEach((declaration) => this.addDeclaration(declaration));
451- rule.value.declarations?.importantDeclarations?.forEach((declaration) => this.addDeclaration(declaration, true));
452- rule.value.rules?.forEach((rule2) => this.parseRuleRec(rule2));
453- return;
454- }
455- let rtl = null;
456- let theme = null;
457- let active = null;
458- let focus = null;
459- let disabled = null;
460- let dataAttributes = null;
461- selector.forEach((selector2) => {
462- if (selector2.type === "pseudo-class" && selector2.kind === "where") {
463- selector2.selectors.forEach((selector3) => {
464- selector3.forEach((selector4) => {
465- if (selector4.type === "class" && this.bundlerConfig.themes.includes(selector4.name)) {
466- theme = selector4.name;
467- }
468- if (selector4.type === "pseudo-class" && selector4.kind === "dir") {
469- rtl = selector4.direction === "rtl";
470- }
471- });
472- });
473- }
474- if (selector2.type === "pseudo-class" && selector2.kind === "active") {
475- active = true;
476- }
477- if (selector2.type === "pseudo-class" && selector2.kind === "focus") {
478- focus = true;
479+ const trailing = selector.slice(1);
480+ const { variants, hasVariants } = this.readSelectorVariants(trailing);
481+ if (trailing.length > 0 && !hasVariants) {
482+ return;
483 }
484- if (selector2.type === "pseudo-class" && selector2.kind === "disabled") {
485- disabled = true;
486- }
487- if (selector2.type === "attribute" && selector2.operation === null && selector2.name.startsWith("data-")) {
488- dataAttributes ??= {};
489- dataAttributes[selector2.name] = `"true"`;
490- }
491- if (selector2.type === "attribute" && selector2.operation?.operator === "equal" && selector2.name.startsWith("data-")) {
492- dataAttributes ??= {};
493- dataAttributes[selector2.name] = `"${selector2.operation.value}"`;
494+ const parseClassRule = () => {
495+ rule.value.declarations?.declarations?.forEach((declaration) => this.addDeclaration(declaration));
496+ rule.value.declarations?.importantDeclarations?.forEach((declaration) => this.addDeclaration(declaration, true));
497+ rule.value.rules?.forEach((rule2) => this.parseRuleRec(rule2));
498+ };
499+ if (hasVariants) {
500+ this.withSelectorVariants(variants, parseClassRule);
501+ } else {
502+ parseClassRule();
503 }
504- });
505- if ([rtl, theme, active, focus, disabled, dataAttributes].some(isDefined)) {
506- this.declarationConfig.rtl ??= rtl;
507- this.declarationConfig.theme ??= theme;
508- this.declarationConfig.active ??= active;
509- this.declarationConfig.focus ??= focus;
510- this.declarationConfig.disabled ??= disabled;
511- this.declarationConfig.dataAttributes ??= dataAttributes;
512- rule.value.declarations?.declarations?.forEach((declaration) => this.addDeclaration(declaration));
513- rule.value.declarations?.importantDeclarations?.forEach((declaration) => this.addDeclaration(declaration, true));
514- rule.value.rules?.forEach((rule2) => this.parseRuleRec(rule2));
515- this.declarationConfig.rtl = null;
516- this.declarationConfig.theme = null;
517- this.declarationConfig.active = null;
518- this.declarationConfig.focus = null;
519- this.declarationConfig.disabled = null;
520- this.declarationConfig.dataAttributes = null;
521+ return;
522+ }
523+ const { variants, hasVariants } = this.readSelectorVariants(selector);
524+ if (hasVariants) {
525+ this.withSelectorVariants(variants, () => {
526+ rule.value.declarations?.declarations?.forEach((declaration) => this.addDeclaration(declaration));
527+ rule.value.declarations?.importantDeclarations?.forEach((declaration) => this.addDeclaration(declaration, true));
528+ rule.value.rules?.forEach((rule2) => this.parseRuleRec(rule2));
529+ });
530 return;
531 }
532 selector.forEach((selectorToken) => {
533@@ -1622,11 +1647,13 @@ class ProcessorBuilder {
534 }
535 if (rule.type === "media") {
536 const { mediaQueries } = rule.value.query;
537- this.declarationConfig.mediaQueries.push(...mediaQueries);
538+ const outerConfig = this.declarationConfig;
539+ const blockMediaQueries = [...outerConfig.mediaQueries, ...mediaQueries];
540 rule.value.rules.forEach((rule2) => {
541+ this.declarationConfig = { ...outerConfig, mediaQueries: blockMediaQueries };
542 this.parseRuleRec(rule2);
543- this.declarationConfig = this.getDeclarationConfig();
544 });
545+ this.declarationConfig = outerConfig;
546 return;
547 }
548 if (rule.type === "layer-block") {
549@@ -1746,10 +1773,11 @@ const transform = async (config, projectRoot, filePath, data, options) => {
550 await bundlerConfig.generateArtifacts(cssArtifactPath);
551 const virtualCode = await compileCSS(bundlerConfig);
552 const isWeb = bundlerConfig.platform === Platform.Web;
553+ const nativeStylesFingerprint = isWeb ? void 0 : createHash("sha256").update(virtualCode).update("\0").update(bundlerConfig.stringifiedThemes).digest("hex");
554 data = Buffer.from(
555 isWeb ? virtualCode : [
556 `const { Uniwind } = require('uniwind');`,
557- `Uniwind.__reinit(rt => ${virtualCode}, ${bundlerConfig.stringifiedThemes});`
558+ `Uniwind.__reinit(rt => ${virtualCode}, ${bundlerConfig.stringifiedThemes}, '${nativeStylesFingerprint}');`
559 ].join(""),
560 "utf-8"
561 );
562diff --git a/dist/module/bundler/css-processor/processor.js b/dist/module/bundler/css-processor/processor.js
563index 58c96b6..6849ca7 100644
564--- a/dist/module/bundler/css-processor/processor.js
565+++ b/dist/module/bundler/css-processor/processor.js
566@@ -99,6 +99,64 @@ export class ProcessorBuilder {
567 style.importantProperties.push(declaration.property);
568 }
569 }
570+ readSelectorVariants(tokens) {
571+ const variants = {
572+ rtl: null,
573+ theme: null,
574+ active: null,
575+ focus: null,
576+ disabled: null,
577+ dataAttributes: null
578+ };
579+ tokens.forEach((token) => {
580+ if (token.type === "pseudo-class" && token.kind === "where") {
581+ token.selectors.forEach((compound) => {
582+ compound.forEach((inner) => {
583+ if (inner.type === "class" && this.bundlerConfig.themes.includes(inner.name)) {
584+ variants.theme = inner.name;
585+ }
586+ if (inner.type === "pseudo-class" && inner.kind === "dir") {
587+ variants.rtl = inner.direction === "rtl";
588+ }
589+ });
590+ });
591+ }
592+ if (token.type === "pseudo-class" && token.kind === "active") {
593+ variants.active = true;
594+ }
595+ if (token.type === "pseudo-class" && token.kind === "focus") {
596+ variants.focus = true;
597+ }
598+ if (token.type === "pseudo-class" && token.kind === "disabled") {
599+ variants.disabled = true;
600+ }
601+ if (token.type === "attribute" && token.operation === null && token.name.startsWith("data-")) {
602+ variants.dataAttributes ??= {};
603+ variants.dataAttributes[token.name] = `"true"`;
604+ }
605+ if (token.type === "attribute" && token.operation?.operator === "equal" && token.name.startsWith("data-")) {
606+ variants.dataAttributes ??= {};
607+ variants.dataAttributes[token.name] = `"${token.operation.value}"`;
608+ }
609+ });
610+ const hasVariants = Object.values(variants).some(isDefined);
611+ return { variants, hasVariants };
612+ }
613+ withSelectorVariants(variants, run) {
614+ this.declarationConfig.rtl ??= variants.rtl;
615+ this.declarationConfig.theme ??= variants.theme;
616+ this.declarationConfig.active ??= variants.active;
617+ this.declarationConfig.focus ??= variants.focus;
618+ this.declarationConfig.disabled ??= variants.disabled;
619+ this.declarationConfig.dataAttributes ??= variants.dataAttributes;
620+ run();
621+ this.declarationConfig.rtl = null;
622+ this.declarationConfig.theme = null;
623+ this.declarationConfig.active = null;
624+ this.declarationConfig.focus = null;
625+ this.declarationConfig.disabled = null;
626+ this.declarationConfig.dataAttributes = null;
627+ }
628 parseRuleRec(rule) {
629 if (this.declarationConfig.className !== null) {
630 const lastStyle = this.stylesheets[this.declarationConfig.className]?.at(-1);
631@@ -114,64 +172,30 @@ export class ProcessorBuilder {
632 this.declarationConfig.className = newClassName;
633 this.stylesheets[newClassName] ??= [];
634 this.stylesheets[newClassName].push({});
635- rule.value.declarations?.declarations?.forEach((declaration) => this.addDeclaration(declaration));
636- rule.value.declarations?.importantDeclarations?.forEach((declaration) => this.addDeclaration(declaration, true));
637- rule.value.rules?.forEach((rule2) => this.parseRuleRec(rule2));
638- return;
639- }
640- let rtl = null;
641- let theme = null;
642- let active = null;
643- let focus = null;
644- let disabled = null;
645- let dataAttributes = null;
646- selector.forEach((selector2) => {
647- if (selector2.type === "pseudo-class" && selector2.kind === "where") {
648- selector2.selectors.forEach((selector3) => {
649- selector3.forEach((selector4) => {
650- if (selector4.type === "class" && this.bundlerConfig.themes.includes(selector4.name)) {
651- theme = selector4.name;
652- }
653- if (selector4.type === "pseudo-class" && selector4.kind === "dir") {
654- rtl = selector4.direction === "rtl";
655- }
656- });
657- });
658- }
659- if (selector2.type === "pseudo-class" && selector2.kind === "active") {
660- active = true;
661+ const trailing = selector.slice(1);
662+ const { variants, hasVariants } = this.readSelectorVariants(trailing);
663+ if (trailing.length > 0 && !hasVariants) {
664+ return;
665 }
666- if (selector2.type === "pseudo-class" && selector2.kind === "focus") {
667- focus = true;
668- }
669- if (selector2.type === "pseudo-class" && selector2.kind === "disabled") {
670- disabled = true;
671- }
672- if (selector2.type === "attribute" && selector2.operation === null && selector2.name.startsWith("data-")) {
673- dataAttributes ??= {};
674- dataAttributes[selector2.name] = `"true"`;
675- }
676- if (selector2.type === "attribute" && selector2.operation?.operator === "equal" && selector2.name.startsWith("data-")) {
677- dataAttributes ??= {};
678- dataAttributes[selector2.name] = `"${selector2.operation.value}"`;
679+ const parseClassRule = () => {
680+ rule.value.declarations?.declarations?.forEach((declaration) => this.addDeclaration(declaration));
681+ rule.value.declarations?.importantDeclarations?.forEach((declaration) => this.addDeclaration(declaration, true));
682+ rule.value.rules?.forEach((rule2) => this.parseRuleRec(rule2));
683+ };
684+ if (hasVariants) {
685+ this.withSelectorVariants(variants, parseClassRule);
686+ } else {
687+ parseClassRule();
688 }
689- });
690- if ([rtl, theme, active, focus, disabled, dataAttributes].some(isDefined)) {
691- this.declarationConfig.rtl ??= rtl;
692- this.declarationConfig.theme ??= theme;
693- this.declarationConfig.active ??= active;
694- this.declarationConfig.focus ??= focus;
695- this.declarationConfig.disabled ??= disabled;
696- this.declarationConfig.dataAttributes ??= dataAttributes;
697- rule.value.declarations?.declarations?.forEach((declaration) => this.addDeclaration(declaration));
698- rule.value.declarations?.importantDeclarations?.forEach((declaration) => this.addDeclaration(declaration, true));
699- rule.value.rules?.forEach((rule2) => this.parseRuleRec(rule2));
700- this.declarationConfig.rtl = null;
701- this.declarationConfig.theme = null;
702- this.declarationConfig.active = null;
703- this.declarationConfig.focus = null;
704- this.declarationConfig.disabled = null;
705- this.declarationConfig.dataAttributes = null;
706+ return;
707+ }
708+ const { variants, hasVariants } = this.readSelectorVariants(selector);
709+ if (hasVariants) {
710+ this.withSelectorVariants(variants, () => {
711+ rule.value.declarations?.declarations?.forEach((declaration) => this.addDeclaration(declaration));
712+ rule.value.declarations?.importantDeclarations?.forEach((declaration) => this.addDeclaration(declaration, true));
713+ rule.value.rules?.forEach((rule2) => this.parseRuleRec(rule2));
714+ });
715 return;
716 }
717 selector.forEach((selectorToken) => {
718@@ -191,11 +215,13 @@ export class ProcessorBuilder {
719 }
720 if (rule.type === "media") {
721 const { mediaQueries } = rule.value.query;
722- this.declarationConfig.mediaQueries.push(...mediaQueries);
723+ const outerConfig = this.declarationConfig;
724+ const blockMediaQueries = [...outerConfig.mediaQueries, ...mediaQueries];
725 rule.value.rules.forEach((rule2) => {
726+ this.declarationConfig = { ...outerConfig, mediaQueries: blockMediaQueries };
727 this.parseRuleRec(rule2);
728- this.declarationConfig = this.getDeclarationConfig();
729 });
730+ this.declarationConfig = outerConfig;
731 return;
732 }
733 if (rule.type === "layer-block") {
734diff --git a/src/bundler/adapters/metro/transformer.ts b/src/bundler/adapters/metro/transformer.ts
735index 08059b0..05c0515 100644
736--- a/src/bundler/adapters/metro/transformer.ts
737+++ b/src/bundler/adapters/metro/transformer.ts
738@@ -5,6 +5,7 @@ import { Platform } from '@/common/consts'
739 import type * as ExpoMetroConfig from '@expo/metro-config'
740 import type * as MetroTransformWorker from 'metro-transform-worker'
741 import type { JsTransformerConfig, JsTransformOptions } from 'metro-transform-worker'
742+import { createHash } from 'node:crypto'
743 import path from 'path'
744
745 const cssArtifactPath = path.resolve(__dirname, '../../uniwind.css')
746@@ -69,13 +70,20 @@ export const transform = async (
747 await bundlerConfig.generateArtifacts(cssArtifactPath)
748 const virtualCode = await compileCSS(bundlerConfig)
749 const isWeb = bundlerConfig.platform === Platform.Web
750+ const nativeStylesFingerprint = isWeb
751+ ? undefined
752+ : createHash('sha256')
753+ .update(virtualCode)
754+ .update('\0')
755+ .update(bundlerConfig.stringifiedThemes)
756+ .digest('hex')
757
758 data = Buffer.from(
759 isWeb
760 ? virtualCode
761 : [
762 `const { Uniwind } = require('uniwind');`,
763- `Uniwind.__reinit(rt => ${virtualCode}, ${bundlerConfig.stringifiedThemes});`,
764+ `Uniwind.__reinit(rt => ${virtualCode}, ${bundlerConfig.stringifiedThemes}, '${nativeStylesFingerprint}');`,
765 ].join(''),
766 'utf-8',
767 )
768diff --git a/src/bundler/css-processor/processor.ts b/src/bundler/css-processor/processor.ts
769index c8b3bb8..4af4199 100644
770--- a/src/bundler/css-processor/processor.ts
771+++ b/src/bundler/css-processor/processor.ts
772@@ -123,6 +123,77 @@ export class ProcessorBuilder {
773 }
774 }
775
776+ private readSelectorVariants(tokens: ReadonlyArray<any>) {
777+ const variants = {
778+ rtl: null as boolean | null,
779+ theme: null as string | null,
780+ active: null as boolean | null,
781+ focus: null as boolean | null,
782+ disabled: null as boolean | null,
783+ dataAttributes: null as Record<string, string> | null,
784+ }
785+
786+ tokens.forEach(token => {
787+ if (token.type === 'pseudo-class' && token.kind === 'where') {
788+ token.selectors.forEach((compound: Array<any>) => {
789+ compound.forEach(inner => {
790+ if (inner.type === 'class' && this.bundlerConfig.themes.includes(inner.name)) {
791+ variants.theme = inner.name
792+ }
793+
794+ if (inner.type === 'pseudo-class' && inner.kind === 'dir') {
795+ variants.rtl = inner.direction === 'rtl'
796+ }
797+ })
798+ })
799+ }
800+
801+ if (token.type === 'pseudo-class' && token.kind === 'active') {
802+ variants.active = true
803+ }
804+
805+ if (token.type === 'pseudo-class' && token.kind === 'focus') {
806+ variants.focus = true
807+ }
808+
809+ if (token.type === 'pseudo-class' && token.kind === 'disabled') {
810+ variants.disabled = true
811+ }
812+
813+ // data-x
814+ if (token.type === 'attribute' && token.operation === null && token.name.startsWith('data-')) {
815+ variants.dataAttributes ??= {}
816+ variants.dataAttributes[token.name] = `"true"`
817+ }
818+
819+ // data-x=
820+ if (token.type === 'attribute' && token.operation?.operator === 'equal' && token.name.startsWith('data-')) {
821+ variants.dataAttributes ??= {}
822+ variants.dataAttributes[token.name] = `"${token.operation.value}"`
823+ }
824+ })
825+
826+ return { variants, hasVariants: Object.values(variants).some(isDefined) }
827+ }
828+
829+ private withSelectorVariants(variants: ReturnType<ProcessorBuilder['readSelectorVariants']>['variants'], run: () => void) {
830+ this.declarationConfig.rtl ??= variants.rtl
831+ this.declarationConfig.theme ??= variants.theme
832+ this.declarationConfig.active ??= variants.active
833+ this.declarationConfig.focus ??= variants.focus
834+ this.declarationConfig.disabled ??= variants.disabled
835+ this.declarationConfig.dataAttributes ??= variants.dataAttributes
836+
837+ run()
838+
839+ this.declarationConfig.rtl = null
840+ this.declarationConfig.theme = null
841+ this.declarationConfig.active = null
842+ this.declarationConfig.focus = null
843+ this.declarationConfig.disabled = null
844+ this.declarationConfig.dataAttributes = null
845+ }
846+
847 private parseRuleRec(rule: Rule<Declaration, MediaQuery>) {
848 if (this.declarationConfig.className !== null) {
849 const lastStyle = this.stylesheets[this.declarationConfig.className]?.at(-1)
850@@ -142,78 +213,41 @@ export class ProcessorBuilder {
851 this.stylesheets[newClassName] ??= []
852 this.stylesheets[newClassName].push({})
853
854- rule.value.declarations?.declarations?.forEach(declaration => this.addDeclaration(declaration))
855- rule.value.declarations?.importantDeclarations?.forEach(declaration => this.addDeclaration(declaration, true))
856- rule.value.rules?.forEach(rule => this.parseRuleRec(rule))
857+ // Tailwind >= 4.3.3 emits `.active\:x:active {}` instead of
858+ // nesting `&:active` under the class, so the variant tokens
859+ // sit right after the class token. A compound the runtime
860+ // cannot express (e.g. `[aria-disabled="true"]`) used to be
861+ // an empty nested rule and must not become unconditional.
862+ const trailing = selector.slice(1)
863+ const { variants, hasVariants } = this.readSelectorVariants(trailing)
864
865- return
866- }
867-
868- let rtl = null as boolean | null
869- let theme = null as string | null
870- let active = null as boolean | null
871- let focus = null as boolean | null
872- let disabled = null as boolean | null
873- let dataAttributes = null as Record<string, string> | null
874-
875- selector.forEach(selector => {
876- if (selector.type === 'pseudo-class' && selector.kind === 'where') {
877- selector.selectors.forEach(selector => {
878- selector.forEach(selector => {
879- if (selector.type === 'class' && this.bundlerConfig.themes.includes(selector.name)) {
880- theme = selector.name
881- }
882-
883- if (selector.type === 'pseudo-class' && selector.kind === 'dir') {
884- rtl = selector.direction === 'rtl'
885- }
886- })
887- })
888+ if (trailing.length > 0 && !hasVariants) {
889+ return
890 }
891
892- if (selector.type === 'pseudo-class' && selector.kind === 'active') {
893- active = true
894- }
895-
896- if (selector.type === 'pseudo-class' && selector.kind === 'focus') {
897- focus = true
898+ const parseClassRule = () => {
899+ rule.value.declarations?.declarations?.forEach(declaration => this.addDeclaration(declaration))
900+ rule.value.declarations?.importantDeclarations?.forEach(declaration => this.addDeclaration(declaration, true))
901+ rule.value.rules?.forEach(rule => this.parseRuleRec(rule))
902 }
903
904- if (selector.type === 'pseudo-class' && selector.kind === 'disabled') {
905- disabled = true
906+ if (hasVariants) {
907+ this.withSelectorVariants(variants, parseClassRule)
908+ } else {
909+ parseClassRule()
910 }
911
912- // data-x
913- if (selector.type === 'attribute' && selector.operation === null && selector.name.startsWith('data-')) {
914- dataAttributes ??= {}
915- dataAttributes[selector.name] = `"true"`
916- }
917+ return
918+ }
919
920- // data-x=
921- if (selector.type === 'attribute' && selector.operation?.operator === 'equal' && selector.name.startsWith('data-')) {
922- dataAttributes ??= {}
923- dataAttributes[selector.name] = `"${selector.operation.value}"`
924- }
925- })
926+ const { variants, hasVariants } = this.readSelectorVariants(selector)
927
928- if ([rtl, theme, active, focus, disabled, dataAttributes].some(isDefined)) {
929- this.declarationConfig.rtl ??= rtl
930- this.declarationConfig.theme ??= theme
931- this.declarationConfig.active ??= active
932- this.declarationConfig.focus ??= focus
933- this.declarationConfig.disabled ??= disabled
934- this.declarationConfig.dataAttributes ??= dataAttributes
935-
936- rule.value.declarations?.declarations?.forEach(declaration => this.addDeclaration(declaration))
937- rule.value.declarations?.importantDeclarations?.forEach(declaration => this.addDeclaration(declaration, true))
938- rule.value.rules?.forEach(rule => this.parseRuleRec(rule))
939-
940- this.declarationConfig.rtl = null
941- this.declarationConfig.theme = null
942- this.declarationConfig.active = null
943- this.declarationConfig.focus = null
944- this.declarationConfig.disabled = null
945- this.declarationConfig.dataAttributes = null
946+ if (hasVariants) {
947+ this.withSelectorVariants(variants, () => {
948+ rule.value.declarations?.declarations?.forEach(declaration => this.addDeclaration(declaration))
949+ rule.value.declarations?.importantDeclarations?.forEach(declaration => this.addDeclaration(declaration, true))
950+ rule.value.rules?.forEach(rule => this.parseRuleRec(rule))
951+ })
952
953 return
954 }
955@@ -241,13 +275,21 @@ export class ProcessorBuilder {
956
957 if (rule.type === 'media') {
958 const { mediaQueries } = rule.value.query
959+ // Media queries have to outlive the reset between the block's sibling
960+ // rules: resetting only after each parse left every rule past the first
961+ // without the block's queries, shipping ios:/android: variants and later
962+ // utilities of a width breakpoint unguarded to every platform. Reset to
963+ // the outer config instead of a fresh one so media nested inside a class
964+ // rule keeps writing into that class.
965+ const outerConfig = this.declarationConfig
966+ const blockMediaQueries = [...outerConfig.mediaQueries, ...mediaQueries]
967
968- this.declarationConfig.mediaQueries.push(...mediaQueries)
969 rule.value.rules.forEach(rule => {
970+ this.declarationConfig = { ...outerConfig, mediaQueries: blockMediaQueries }
971 this.parseRuleRec(rule)
972- this.declarationConfig = this.getDeclarationConfig()
973 })
974
975+ this.declarationConfig = outerConfig
976 return
977 }
978
979diff --git a/src/core/config/config.native.ts b/src/core/config/config.native.ts
980index 5e0832b..01aed1b 100644
981--- a/src/core/config/config.native.ts
982+++ b/src/core/config/config.native.ts
983@@ -8,6 +8,8 @@ import type { CSSVariables, GenerateStyleSheetsCallback, ThemeName } from '../ty
984 import { UniwindConfigBuilder as UniwindConfigBuilderBase } from './config.common'
985
986 class UniwindConfigBuilder extends UniwindConfigBuilderBase {
987+ private stylesFingerprint: string | undefined
988+
989 constructor() {
990 super()
991 }
992@@ -35,9 +37,18 @@ class UniwindConfigBuilder extends UniwindConfigBuilderBase {
993 UniwindListener.notify([StyleDependency.Insets])
994 }
995
996- protected __reinit(generateStyleSheetCallback: GenerateStyleSheetsCallback, themes: Array<string>) {
997+ protected __reinit(
998+ generateStyleSheetCallback: GenerateStyleSheetsCallback,
999+ themes: Array<string>,
1000+ stylesFingerprint?: string,
1001+ ) {
1002+ if (__DEV__ && stylesFingerprint !== undefined && stylesFingerprint === this.stylesFingerprint) {
1003+ return
1004+ }
1005+
1006 super.__reinit(generateStyleSheetCallback, themes)
1007 UniwindStore.reinit(generateStyleSheetCallback, themes)
1008+ this.stylesFingerprint = stylesFingerprint
1009 }
1010
1011 protected onThemeChange() {
pnpm-lock.yaml+5-6
...@@ -115,7 +115,6 @@ patchedDependencies:...@@ -115,7 +115,6 @@ patchedDependencies:
115 react-native-nitro-markdown@0.5.8: 642b47830730acff3761fe29cf54271d18b493c0070c750097850b7ea032e00c115 react-native-nitro-markdown@0.5.8: 642b47830730acff3761fe29cf54271d18b493c0070c750097850b7ea032e00c
116 react-native-nitro-modules@0.35.9: daf4a639ce3f951af630a3711e4082d58e3a11e5a79f45efb0b59925327ebde2116 react-native-nitro-modules@0.35.9: daf4a639ce3f951af630a3711e4082d58e3a11e5a79f45efb0b59925327ebde2
117 react-native-screens@4.28.0: cc1301436ee62c39860acc9ee1a1e60b7d47e6abd211c42effa62c056d165483117 react-native-screens@4.28.0: cc1301436ee62c39860acc9ee1a1e60b7d47e6abd211c42effa62c056d165483
118 uniwind@1.11.0: a91f3953483cfd434436de6358ff5b63c49974ef148fad91246c26ba95332625
119118
120importers:119importers:
121120
...@@ -487,8 +486,8 @@ importers:...@@ -487,8 +486,8 @@ importers:
487 specifier: ^3.5.0486 specifier: ^3.5.0
488 version: 3.6.0487 version: 3.6.0
489 uniwind:488 uniwind:
490 specifier: 1.11.0489 specifier: 1.12.1
491 version: 1.11.0(patch_hash=a91f3953483cfd434436de6358ff5b63c49974ef148fad91246c26ba95332625)(@expo/metro-config@58.0.6(patch_hash=90fa980e8b6480631fb2fbb04559834e3c612db40c550cd828f11be297d8d760)(bufferutil@4.1.0)(expo@58.0.2)(typescript@7.0.2)(utf-8-validate@6.0.6))(metro-cache@0.87.1)(metro-transform-worker@0.87.1(bufferutil@4.1.0)(utf-8-validate@6.0.6))(metro@0.87.1(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0)(tailwindcss@4.3.3)490 version: 1.12.1(@expo/metro-config@58.0.6(patch_hash=90fa980e8b6480631fb2fbb04559834e3c612db40c550cd828f11be297d8d760)(bufferutil@4.1.0)(expo@58.0.2)(typescript@7.0.2)(utf-8-validate@6.0.6))(metro-cache@0.87.1)(metro-transform-worker@0.87.1(bufferutil@4.1.0)(utf-8-validate@6.0.6))(metro@0.87.1(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0)(tailwindcss@4.3.3)
492 devDependencies:491 devDependencies:
493 '@effect/vitest':492 '@effect/vitest':
494 specifier: 4.0.0-rc.115493 specifier: 4.0.0-rc.115
...@@ -10905,8 +10904,8 @@ packages:...@@ -10905,8 +10904,8 @@ packages:
10905 resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}10904 resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
10906 engines: {node: '>= 10.0.0'}10905 engines: {node: '>= 10.0.0'}
1090710906
10908 uniwind@1.11.0:10907 uniwind@1.12.1:
10909 resolution: {integrity: sha512-e2mYlHrZzAQtZUf3STrfVDTetspv7p5vm/cu6p8kjLRQaL9r6QFG+MlwZ3hrI2LHnzgzjbSo+MwvoE380zz2JQ==}10908 resolution: {integrity: sha512-u0OVMVqefGchOpsPIwIYvcF2Jl9JtVVaXBb+wfqAYoyLrhN27Ll50aQ7aFT81k73JSq+He6dNlcbTl2Qb9cTMw==}
10910 hasBin: true10909 hasBin: true
10911 peerDependencies:10910 peerDependencies:
10912 '@expo/metro-config': 58.0.610911 '@expo/metro-config': 58.0.6
...@@ -22081,7 +22080,7 @@ snapshots:...@@ -22081,7 +22080,7 @@ snapshots:
2208122080
22082 universalify@2.0.1: {}22081 universalify@2.0.1: {}
2208322082
22084 uniwind@1.11.0(patch_hash=a91f3953483cfd434436de6358ff5b63c49974ef148fad91246c26ba95332625)(@expo/metro-config@58.0.6(patch_hash=90fa980e8b6480631fb2fbb04559834e3c612db40c550cd828f11be297d8d760)(bufferutil@4.1.0)(expo@58.0.2)(typescript@7.0.2)(utf-8-validate@6.0.6))(metro-cache@0.87.1)(metro-transform-worker@0.87.1(bufferutil@4.1.0)(utf-8-validate@6.0.6))(metro@0.87.1(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0)(tailwindcss@4.3.3):22083 uniwind@1.12.1(@expo/metro-config@58.0.6(patch_hash=90fa980e8b6480631fb2fbb04559834e3c612db40c550cd828f11be297d8d760)(bufferutil@4.1.0)(expo@58.0.2)(typescript@7.0.2)(utf-8-validate@6.0.6))(metro-cache@0.87.1)(metro-transform-worker@0.87.1(bufferutil@4.1.0)(utf-8-validate@6.0.6))(metro@0.87.1(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0)(tailwindcss@4.3.3):
22085 dependencies:22084 dependencies:
22086 '@tailwindcss/node': 4.3.322085 '@tailwindcss/node': 4.3.3
22087 '@tailwindcss/oxide': 4.3.322086 '@tailwindcss/oxide': 4.3.3
pnpm-workspace.yaml+1-1
...@@ -125,6 +125,7 @@ minimumReleaseAgeExclude:...@@ -125,6 +125,7 @@ minimumReleaseAgeExclude:
125 - "@distilled.cloud/stripe@1.0.0-rc.12"125 - "@distilled.cloud/stripe@1.0.0-rc.12"
126 - "@shadcn/lint@0.1.5"126 - "@shadcn/lint@0.1.5"
127 - cn@0.3.2127 - cn@0.3.2
128 - uniwind@1.12.1
128 - "@expo/dom-webview@58.0.2"129 - "@expo/dom-webview@58.0.2"
129 - "expo-auth-session@58.0.7"130 - "expo-auth-session@58.0.7"
130 - "expo-camera@58.0.7"131 - "expo-camera@58.0.7"
...@@ -263,7 +264,6 @@ patchedDependencies:...@@ -263,7 +264,6 @@ patchedDependencies:
263 react-native-keyboard-controller@1.22.6: patches/react-native-keyboard-controller@1.22.6.patch264 react-native-keyboard-controller@1.22.6: patches/react-native-keyboard-controller@1.22.6.patch
264 react-native-nitro-modules@0.35.9: patches/react-native-nitro-modules@0.35.9.patch265 react-native-nitro-modules@0.35.9: patches/react-native-nitro-modules@0.35.9.patch
265 react-native-screens@4.28.0: patches/react-native-screens@4.28.0.patch266 react-native-screens@4.28.0: patches/react-native-screens@4.28.0.patch
266 uniwind@1.11.0: patches/uniwind@1.11.0.patch
267267
268peerDependencyRules:268peerDependencyRules:
269 allowAny:269 allowAny: