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..48ea3cf5 --- /dev/null +++ b/src/__tests__/native/root-variable-registry.test.ts @@ -0,0 +1,28 @@ +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", async () => { + // 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 = await import("../../native-internal/root"); + + expect(firstCopy.rootVariables("__rn-css-rem").get()).toBe(14); + firstCopy.rootVariables("__rn-css-rem").set([[16]]); + + jest.resetModules(); + const secondCopy = await import("../../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;