Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
10 changes: 9 additions & 1 deletion packages/uniwind/src/bundler/adapters/metro/transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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',
)
Expand Down
13 changes: 12 additions & 1 deletion packages/uniwind/src/core/config/config.native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down Expand Up @@ -35,9 +37,18 @@ class UniwindConfigBuilder extends UniwindConfigBuilderBase {
UniwindListener.notify([StyleDependency.Insets])
}

protected __reinit(generateStyleSheetCallback: GenerateStyleSheetsCallback, themes: Array<string>) {
protected __reinit(
generateStyleSheetCallback: GenerateStyleSheetsCallback,
themes: Array<string>,
stylesFingerprint?: string,
) {
if (__DEV__ && stylesFingerprint !== undefined && stylesFingerprint === this.stylesFingerprint) {
return
}

super.__reinit(generateStyleSheetCallback, themes)
UniwindStore.reinit(generateStyleSheetCallback, themes)
this.stylesFingerprint = stylesFingerprint
}

protected onThemeChange() {
Expand Down
52 changes: 52 additions & 0 deletions packages/uniwind/tests/native/core/config.test.ts
Original file line number Diff line number Diff line change
@@ -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<string>, 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)
})
})