Skip to content

fix(native): share the :root variable registries across a dual-package split - #410

Open
YevheniiKotyrlo wants to merge 2 commits into
nativewind:mainfrom
YevheniiKotyrlo:fix/root-variable-registry-dual-package
Open

YevheniiKotyrlo wants to merge 2 commits into
nativewind:mainfrom
YevheniiKotyrlo:fix/root-variable-registry-dual-package

Conversation

@YevheniiKotyrlo

@YevheniiKotyrlo YevheniiKotyrlo commented Aug 14, 2026 •

Copy link
Copy Markdown
Contributor

Problem

The exports map gives import and require different builds, and Metro resolves that condition per requesting module. A bundle that pulls react-native-css from both a CommonJS dependency and ES-module source therefore evaluates native-internal/root.ts twice, and each copy creates its own :root variable registries. The compiled stylesheet is injected once, into the copy the app's .css module resolved, so every component resolving through the other copy reads var() from empty registries and renders without the value.

expo-router's Link is one such component: its compiled CommonJS binds dist/commonjs, while app source binds dist/module. A text-danger-foreground link on a dark surface painted rgb(0, 0, 0) instead of rgb(251, 44, 54) on Android. I measured that on 3.0.7. main keeps the same exports map and the same module-scope registries.

style-collection.ts and variables.tsx already keep their state on globalThis for this reason. root.ts does not.

Solution

The registries live on globalThis, as their siblings' state does, and are created and seeded in one step. The seed writes --__rn-css-rem and the platform colour. If only the creation were shared, a copy that loads after the stylesheet inject would run the seed again and reset a project's :root { font-size: 16px } to 14.

Tests

src/__tests__/native/root-variable-registry.test.ts:

  • The module's exports are the registries published on globalThis.
  • A second copy, evaluated with jest.resetModules() against the same globalThis, binds the same registries and keeps a rem the first copy set to 16.

On main both fail; both pass here. Creating the registries in every copy fails the second test.

Verification

On Windows with Node 26:

  • yarn lint clean
  • yarn typecheck clean
  • yarn test --maxWorkers=2 --coverage: 1333 passed, 4 failed. The 4 are the babel cases that also fail on main on this machine.
  • yarn build clean
  • yarn example expo export --platform web exported
  • nothing unstaged after either build

Fixes #409, which reports the same split on iOS with the inspector showing two rootVariables.

Merge order

This overlaps #392; I'll re-cut whichever of the two lands second.

It also shares lines with #412 (native-internal/root.ts); whichever lands second rebases.

Base

Re-written on main (a5002c5).

@YevheniiKotyrlo

YevheniiKotyrlo commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor Author

No device frame

The defect needs a bundle that loads two copies of this package, so the description carries the colour I measured on a device, and the tests load the second copy with jest.resetModules().

…e split

Re-cut onto main (a5002c5). The test reads the registries through
jest.requireActual, because main's jest runs no dynamic import.
@YevheniiKotyrlo
YevheniiKotyrlo force-pushed the fix/root-variable-registry-dual-package branch from 0a4018c to 0f7e7f5 Compare October 11, 2026 12:35

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Metro bundles ESM and CommonJS runtimes; CSS variables inject into the wrong rootVariables family

1 participant