Skip to content

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

Description

@pauloedurezende

Bug summary

With react-native-css@3.0.7, Expo 57 / React Native 0.86 / Metro 0.84 can bundle both dist/module and dist/commonjs copies of the native runtime. StyleCollection and VariableContext are globalized, but rootVariables / universalVariables are module-local.

The transformed CSS module is CommonJS and StyleCollection.inject() therefore closes over the CommonJS rootVariables family. Components rewritten from react-native use the ESM runtime, whose varResolver() reads the ESM family. The stylesheet rules are registered (because StyleCollection is shared), but any runtime var(...) resolves to undefined.

This makes direct/inlined utility values work while mode-aware CSS-variable-backed colors render transparent.

Version / environment

  • react-native-css: 3.0.7
  • nativewind: 5.0.0-preview.4
  • Expo: 57.0.11
  • React Native: 0.86.2
  • Metro: 0.84.4
  • pnpm: 11.17.0
  • iOS simulator (same architecture should affect Android)

Runtime evidence

Using the React Native CDP inspector against the live app:

__r(337).StyleCollection === __r(1247).StyleCollection
// true

__r(337).rootVariables === __r(1247).rootVariables
// false

__r(337).rootVariables("color-surface-primary").get()
// undefined

__r(1247).rootVariables("color-surface-primary").get()
// "#fff"

Here module 337 is dist/module/native-internal/index.js, while 1247 is dist/commonjs/native-internal/index.js.

Both runtimes are present in the same bundle:

dist/module/native-internal/root.js
dist/commonjs/native-internal/root.js
dist/module/native/styles/variables.js
dist/commonjs/native/styles/variables.js

The CSS class itself is correctly registered:

globalThis.__react_native_css_style_collection
  .styles("bg-utility-neutral-900")
  .get()
// [{ d: [[[{ }, "var", "color-utility-neutral-900", 1], "backgroundColor", 1]], dv: 1, ... }]

Resolving the same rule through each runtime proves the split:

const get = observable => observable.get();
const rules = globalThis.__react_native_css_style_collection
  .styles("bg-utility-neutral-900")
  .get();

__r(730).calculateProps(get, rules).normal
// ESM calculate-props: { style: {} }

__r(1263).calculateProps(get, rules).normal
// CJS calculate-props: { style: { backgroundColor: "#171717" } }

The generated bundle shows the CSS injection module depending on the CommonJS native-internal module, while React Native wrappers and useNativeCss use ESM modules.

Why this happens

src/native-internal/style-collection.ts stores StyleCollection on globalThis, and variables.tsx similarly globalizes VariableContext. But src/native-internal/root.ts creates these per module instance:

export const rootVariables = rootVariableFamily();
export const universalVariables = rootVariableFamily();

So ESM/CJS duplication is survivable for registered styles but not variables.

Metro 0.84 intentionally resolves package conditional exports by source import vs require, so a package containing both forms can legitimately include both targets in one bundle.

Suggested fix

Make the root/universal variable families canonical across module formats, similar to StyleCollection and VariableContext (for example, store both on globalThis), and add a regression test/build fixture where CSS injection comes from the CommonJS export while rendering uses the ESM export.

A package-level alternative would be to expose one canonical React Native target above import/require in the exports map, but globalizing the variable registries would also defend against duplicate package instances.

Metadata

Metadata

Assignees

No one assigned

    Labels

    auto-triagedIssue has been automatically triaged by the auto-triage workflowbugSomething isn't workingconfirmedBug reproduced and confirmed by triageneeds-deep-triageNeeds Tier 2 triage (runtime/simulator, self-hosted runner)

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions