diff --git a/CONTEXT.md b/CONTEXT.md index 5fa2ffb4..aff22056 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -119,7 +119,7 @@ Metro integration: - Metro adds `css` as source extension and removes it from asset extensions. - Metro transformer handles the configured CSS entry file specially. - Metro transformer worker selection is lazy, cached per Expo/non-Expo config type, and follows Expo transformer paths or Expo-specific config markers. -- Native platform CSS transforms into a JS module that calls `Uniwind.__reinit(...)`. +- Native platform CSS transforms into a JS module that calls `Uniwind.__reinit(...)` with a fingerprint of the generated styles and themes. During development, the native runtime skips reinitialization when that fingerprint is unchanged. - Web platform CSS transforms into CSS plus web runtime setup. - Resolver swaps React Native component imports to Uniwind-aware implementations where needed. diff --git a/packages/uniwind/src/bundler/adapters/metro/transformer.ts b/packages/uniwind/src/bundler/adapters/metro/transformer.ts index 08059b0f..da6a0352 100644 --- a/packages/uniwind/src/bundler/adapters/metro/transformer.ts +++ b/packages/uniwind/src/bundler/adapters/metro/transformer.ts @@ -5,6 +5,7 @@ import { Platform } from '@/common/consts' import type * as ExpoMetroConfig from '@expo/metro-config' import type * as MetroTransformWorker from 'metro-transform-worker' import type { JsTransformerConfig, JsTransformOptions } from 'metro-transform-worker' +import { createHash } from 'node:crypto' import path from 'path' const cssArtifactPath = path.resolve(__dirname, '../../uniwind.css') @@ -69,13 +70,20 @@ export const transform = async ( await bundlerConfig.generateArtifacts(cssArtifactPath) const virtualCode = await compileCSS(bundlerConfig) const isWeb = bundlerConfig.platform === Platform.Web + const nativeStylesFingerprint = isWeb + ? undefined + : createHash('sha256') + .update(virtualCode) + .update('\0') + .update(bundlerConfig.stringifiedThemes) + .digest('hex') data = Buffer.from( isWeb ? virtualCode : [ `const { Uniwind } = require('uniwind');`, - `Uniwind.__reinit(rt => ${virtualCode}, ${bundlerConfig.stringifiedThemes});`, + `Uniwind.__reinit(rt => ${virtualCode}, ${bundlerConfig.stringifiedThemes}, '${nativeStylesFingerprint}');`, ].join(''), 'utf-8', ) diff --git a/packages/uniwind/src/core/config/config.native.ts b/packages/uniwind/src/core/config/config.native.ts index 5e0832b5..01aed1be 100644 --- a/packages/uniwind/src/core/config/config.native.ts +++ b/packages/uniwind/src/core/config/config.native.ts @@ -8,6 +8,8 @@ import type { CSSVariables, GenerateStyleSheetsCallback, ThemeName } from '../ty import { UniwindConfigBuilder as UniwindConfigBuilderBase } from './config.common' class UniwindConfigBuilder extends UniwindConfigBuilderBase { + private stylesFingerprint: string | undefined + constructor() { super() } @@ -35,9 +37,18 @@ class UniwindConfigBuilder extends UniwindConfigBuilderBase { UniwindListener.notify([StyleDependency.Insets]) } - protected __reinit(generateStyleSheetCallback: GenerateStyleSheetsCallback, themes: Array) { + protected __reinit( + generateStyleSheetCallback: GenerateStyleSheetsCallback, + themes: Array, + stylesFingerprint?: string, + ) { + if (__DEV__ && stylesFingerprint !== undefined && stylesFingerprint === this.stylesFingerprint) { + return + } + super.__reinit(generateStyleSheetCallback, themes) UniwindStore.reinit(generateStyleSheetCallback, themes) + this.stylesFingerprint = stylesFingerprint } protected onThemeChange() { diff --git a/packages/uniwind/tests/native/core/config.test.ts b/packages/uniwind/tests/native/core/config.test.ts new file mode 100644 index 00000000..ffa85dd8 --- /dev/null +++ b/packages/uniwind/tests/native/core/config.test.ts @@ -0,0 +1,52 @@ +import { Uniwind } from '../../../src/core/config/config.native' +import { UniwindStore } from '../../../src/core/native' +import type { GenerateStyleSheetsCallback } from '../../../src/core/types' + +type UniwindForTest = typeof Uniwind & { + __reinit: (initialize: GenerateStyleSheetsCallback, themes: Array, fingerprint?: string) => void +} + +const uniwind = Uniwind as UniwindForTest +const generateStyles = () => ({ scopedVars: {}, stylesheet: {}, vars: {} }) + +describe('Uniwind native config', () => { + afterEach(() => { + jest.restoreAllMocks() + }) + + test('skips reinitialization when generated styles have not changed', () => { + const reinit = jest.spyOn(UniwindStore, 'reinit') + const initialize = jest.fn(generateStyles) + + uniwind.__reinit(initialize, ['light', 'dark'], 'unchanged-styles') + uniwind.__reinit(initialize, ['light', 'dark'], 'unchanged-styles') + + expect(reinit).toHaveBeenCalledTimes(1) + }) + + test('reinitializes when generated styles change', () => { + const reinit = jest.spyOn(UniwindStore, 'reinit') + const initialize = jest.fn(generateStyles) + + uniwind.__reinit(initialize, ['light', 'dark'], 'styles-before') + uniwind.__reinit(initialize, ['light', 'dark'], 'styles-after') + + expect(reinit).toHaveBeenCalledTimes(2) + }) + + test('retries the same generated styles after initialization fails', () => { + const initialize = jest.fn(generateStyles) + const reinit = jest + .spyOn(UniwindStore, 'reinit') + .mockImplementationOnce(() => { + throw new Error('initialization failed') + }) + + expect(() => uniwind.__reinit(initialize, ['light', 'dark'], 'retry-styles')).toThrow( + 'initialization failed', + ) + uniwind.__reinit(initialize, ['light', 'dark'], 'retry-styles') + + expect(reinit).toHaveBeenCalledTimes(2) + }) +})