From 3a18d76c9d1ab25307b0f8fa4692bdb1bc2e9555 Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Fri, 9 Oct 2026 16:12:28 +0300 Subject: [PATCH 1/2] fix(native): share the :root variable registries across a dual-package split Re-cut onto main (a5002c5). The test reads the registries through jest.requireActual, because main's jest runs no dynamic import. --- .../native/root-variable-registry.test.ts | 32 +++++++++++ src/native-internal/root.ts | 57 ++++++++++++++----- 2 files changed, 76 insertions(+), 13 deletions(-) create mode 100644 src/__tests__/native/root-variable-registry.test.ts diff --git a/src/__tests__/native/root-variable-registry.test.ts b/src/__tests__/native/root-variable-registry.test.ts new file mode 100644 index 00000000..6df3049f --- /dev/null +++ b/src/__tests__/native/root-variable-registry.test.ts @@ -0,0 +1,32 @@ +import { rootVariables, universalVariables } from "../../native-internal/root"; + +test("the module's exports are the registries published on globalThis", () => { + const registries = globalThis.__react_native_css_root_variable_registries; + + expect(registries).toBeDefined(); + expect(rootVariables).toBe(registries?.root); + expect(universalVariables).toBe(registries?.universal); +}); + +test("a second copy of the module shares the registries and does not re-seed", () => { + // jest.resetModules() gives a fresh module registry against the same globalThis, which + // is exactly the dual-package case: the exports map splits import and require onto + // different builds, so two copies of this file evaluate in one bundle + const firstCopy = jest.requireActual< + typeof import("../../native-internal/root") + >("../../native-internal/root"); + + expect(firstCopy.rootVariables("__rn-css-rem").get()).toBe(14); + firstCopy.rootVariables("__rn-css-rem").set([[16]]); + + jest.resetModules(); + const secondCopy = jest.requireActual< + typeof import("../../native-internal/root") + >("../../native-internal/root"); + + // The module body really re-ran, so the assertions below are about two copies + expect(secondCopy).not.toBe(firstCopy); + + expect(secondCopy.rootVariables).toBe(firstCopy.rootVariables); + expect(secondCopy.rootVariables("__rn-css-rem").get()).toBe(16); +}); diff --git a/src/native-internal/root.ts b/src/native-internal/root.ts index e45a7d11..0e86502b 100644 --- a/src/native-internal/root.ts +++ b/src/native-internal/root.ts @@ -29,16 +29,47 @@ const rootVariableFamily = () => { }); }; -export const rootVariables = rootVariableFamily(); -export const universalVariables = rootVariableFamily(); - -rootVariables("__rn-css-rem").set([[14]]); -// eslint-disable-next-line @typescript-eslint/no-unsafe-argument -rootVariables("__rn-css-color").set([ - [ - Platform.OS === "ios" - ? PlatformColor("label", "labelColor") - : PlatformColor("?attr/textColorPrimary", "SystemBaseHighColor"), - ], - // eslint-disable-next-line @typescript-eslint/no-explicit-any -] as any); +interface RootVariableRegistries { + root: ReturnType; + universal: ReturnType; +} + +declare global { + var __react_native_css_root_variable_registries: + | RootVariableRegistries + | undefined; +} + +// Creating and seeding are one step. Guarding only the creation leaves the seeds running +// unconditionally, so a copy initialising after the stylesheet inject clobbers a project's +// `:root { font-size: 16px }` back to 14 +function createRootVariableRegistries(): RootVariableRegistries { + const registries: RootVariableRegistries = { + root: rootVariableFamily(), + universal: rootVariableFamily(), + }; + + registries.root("__rn-css-rem").set([[14]]); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + registries.root("__rn-css-color").set([ + [ + Platform.OS === "ios" + ? PlatformColor("label", "labelColor") + : PlatformColor("?attr/textColorPrimary", "SystemBaseHighColor"), + ], + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ] as any); + + return registries; +} + +// Global, like style-collection.ts and variables.tsx: the exports map splits import and +// require onto different builds and Metro resolves that per requesting module, so a +// compiled-CommonJS dependency and first-party source bind different copies of this file +globalThis.__react_native_css_root_variable_registries ??= + createRootVariableRegistries(); + +export const rootVariables = + globalThis.__react_native_css_root_variable_registries.root; +export const universalVariables = + globalThis.__react_native_css_root_variable_registries.universal; From 0f7e7f5f28cd61d6a3cf46945a65aa2ac07f7e61 Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Fri, 9 Oct 2026 16:19:44 +0300 Subject: [PATCH 2/2] refactor(native): state the registry constraints in one line each --- src/__tests__/native/root-variable-registry.test.ts | 4 +--- src/native-internal/root.ts | 8 ++------ 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/__tests__/native/root-variable-registry.test.ts b/src/__tests__/native/root-variable-registry.test.ts index 6df3049f..700448f9 100644 --- a/src/__tests__/native/root-variable-registry.test.ts +++ b/src/__tests__/native/root-variable-registry.test.ts @@ -9,9 +9,7 @@ test("the module's exports are the registries published on globalThis", () => { }); test("a second copy of the module shares the registries and does not re-seed", () => { - // jest.resetModules() gives a fresh module registry against the same globalThis, which - // is exactly the dual-package case: the exports map splits import and require onto - // different builds, so two copies of this file evaluate in one bundle + // resetModules evaluates the module again against the same globalThis, as a second build in one bundle does const firstCopy = jest.requireActual< typeof import("../../native-internal/root") >("../../native-internal/root"); diff --git a/src/native-internal/root.ts b/src/native-internal/root.ts index 0e86502b..520c0833 100644 --- a/src/native-internal/root.ts +++ b/src/native-internal/root.ts @@ -40,9 +40,7 @@ declare global { | undefined; } -// Creating and seeding are one step. Guarding only the creation leaves the seeds running -// unconditionally, so a copy initialising after the stylesheet inject clobbers a project's -// `:root { font-size: 16px }` back to 14 +// Seeded with the registries, so a copy that loads after the stylesheet inject keeps the project's rem function createRootVariableRegistries(): RootVariableRegistries { const registries: RootVariableRegistries = { root: rootVariableFamily(), @@ -63,9 +61,7 @@ function createRootVariableRegistries(): RootVariableRegistries { return registries; } -// Global, like style-collection.ts and variables.tsx: the exports map splits import and -// require onto different builds and Metro resolves that per requesting module, so a -// compiled-CommonJS dependency and first-party source bind different copies of this file +// The exports map gives import and require different builds, and Metro resolves each per requesting module globalThis.__react_native_css_root_variable_registries ??= createRootVariableRegistries();