diff --git a/package.json b/package.json
index 064df3c6..de1fd22b 100644
--- a/package.json
+++ b/package.json
@@ -31,7 +31,13 @@
"module": "./plugin/index.js",
"default": "./plugin/index.js"
},
- "./package.json": "./package.json"
+ "./package.json": "./package.json",
+ "./server": {
+ "types": "./lib/typescript/src/server/index.d.ts",
+ "module": "./lib/module/server/index",
+ "default": "./lib/commonjs/server/index",
+ "react-native": "./src/server"
+ }
},
"files": [
"src",
@@ -88,6 +94,7 @@
"husky": "9.1.7",
"jest": "29.7.0",
"metro-react-native-babel-preset": "0.77.0",
+ "next": "15.1.1",
"nitro-codegen": "0.18.2",
"react": "18.3.1",
"react-native": "0.76.3",
diff --git a/server/package.json b/server/package.json
new file mode 100644
index 00000000..092e26cb
--- /dev/null
+++ b/server/package.json
@@ -0,0 +1,6 @@
+{
+ "main": "../lib/commonjs/server/index",
+ "module": "../lib/module/server/index",
+ "react-native": "../src/server/index",
+ "types": "../lib/typescript/src/server/index"
+}
diff --git a/src/components/ScopedTheme.tsx b/src/components/ScopedTheme.tsx
index 31e027d1..b2ad83d2 100644
--- a/src/components/ScopedTheme.tsx
+++ b/src/components/ScopedTheme.tsx
@@ -21,7 +21,7 @@ export const ScopedTheme: React.FunctionComponent,
children,
-
+
]
return (
diff --git a/src/components/native/ImageBackground.native.tsx b/src/components/native/ImageBackground.native.tsx
new file mode 100644
index 00000000..93f6e6fe
--- /dev/null
+++ b/src/components/native/ImageBackground.native.tsx
@@ -0,0 +1,4 @@
+import { ImageBackground as NativeImageBackground } from 'react-native'
+import { createUnistylesImageBackground } from '../../core'
+
+export const ImageBackground = createUnistylesImageBackground(NativeImageBackground)
diff --git a/src/components/native/ImageBackground.tsx b/src/components/native/ImageBackground.tsx
index 93f6e6fe..45228d3c 100644
--- a/src/components/native/ImageBackground.tsx
+++ b/src/components/native/ImageBackground.tsx
@@ -1,4 +1,56 @@
+import React from 'react'
import { ImageBackground as NativeImageBackground } from 'react-native'
-import { createUnistylesImageBackground } from '../../core'
+import { forwardRef } from 'react'
+import type { UnistylesValues } from '../../types'
+import { getClassName } from '../../core'
+import { isServer } from '../../web/utils'
+import { UnistylesShadowRegistry } from '../../web'
-export const ImageBackground = createUnistylesImageBackground(NativeImageBackground)
+type Props = {
+ style?: UnistylesValues
+ imageStyle?: UnistylesValues
+}
+
+export const ImageBackground = forwardRef((props, forwardedRef) => {
+ let storedRef: NativeImageBackground | null = null
+ let storedImageRef: NativeImageBackground | null = null
+ const styleClassNames = getClassName(props.style)
+ const imageClassNames = getClassName(props.imageStyle)
+
+ return (
+ // @ts-expect-error - RN types are not compatible with RNW styles
+ {
+ if (!ref) {
+ // @ts-expect-error hidden from TS
+ UnistylesShadowRegistry.remove(storedRef, styleClassNames?.hash)
+ }
+
+ storedRef = ref
+ // @ts-expect-error hidden from TS
+ UnistylesShadowRegistry.add(ref, styleClassNames?.hash)
+
+ if (typeof forwardedRef === 'function') {
+ return forwardedRef(ref)
+ }
+
+ if (forwardedRef) {
+ forwardedRef.current = ref
+ }
+ }}
+ imageRef={isServer() ? undefined : ref => {
+ if (!ref) {
+ // @ts-expect-error hidden from TS
+ UnistylesShadowRegistry.remove(storedImageRef, imageClassNames?.hash)
+ }
+
+ storedImageRef = ref
+ // @ts-expect-error hidden from TS
+ UnistylesShadowRegistry.add(ref, imageClassNames?.hash)
+ }}
+ />
+ )
+})
diff --git a/src/components/native/Pressable.native.tsx b/src/components/native/Pressable.native.tsx
index 273a8909..451eb952 100644
--- a/src/components/native/Pressable.native.tsx
+++ b/src/components/native/Pressable.native.tsx
@@ -22,9 +22,8 @@ export const Pressable = forwardRef(({ variants, style, ..
? unistyles
: [unistyles]
- // @ts-expect-error - this is hidden from TS
+ // @ts-expect-error web types are not compatible with RN styles
UnistylesShadowRegistry.add(ref, styles)
-
storedRef.current = ref
return passForwardedRef(props, ref, forwardedRef)
@@ -45,7 +44,7 @@ export const Pressable = forwardRef(({ variants, style, ..
UnistylesShadowRegistry.selectVariants(variants)
}
- // @ts-expect-error - this is hidden from TS
+ // @ts-expect-error hidden from TS
UnistylesShadowRegistry.remove(storedRef.current)
// @ts-expect-error - this is hidden from TS
UnistylesShadowRegistry.add(storedRef.current, styles)
diff --git a/src/components/native/Pressable.tsx b/src/components/native/Pressable.tsx
index 67bd3958..088651df 100644
--- a/src/components/native/Pressable.tsx
+++ b/src/components/native/Pressable.tsx
@@ -1,7 +1,10 @@
-import React, { forwardRef, useRef } from 'react'
+import React, { forwardRef } from 'react'
import { Pressable as NativePressableReactNative } from 'react-native'
-import type { PressableProps as Props, View, ViewStyle } from 'react-native'
+import type { PressableProps as Props, View } from 'react-native'
import { UnistylesShadowRegistry } from '../../specs'
+import type { UnistylesValues } from '../../types'
+import { getClassName } from '../../core'
+import { isServer } from '../../web/utils'
type Variants = Record
type WebPressableState = {
@@ -10,88 +13,55 @@ type WebPressableState = {
focused: boolean
}
-type WebPressableStyle = ((state: WebPressableState) => ViewStyle) | ViewStyle
+type WebPressableStyle = ((state: WebPressableState) => UnistylesValues) | UnistylesValues
type PressableProps = Props & {
variants?: Variants
style?: WebPressableStyle,
}
-const initialState: WebPressableState = {
- pressed: false,
- hovered: false,
- focused: false
-}
-
-type UpdateStylesProps = {
- ref: View | null,
- style: WebPressableStyle,
- variants?: Variants,
- state: WebPressableState
- scopedTheme?: string
-}
-
-const extractStyleResult = (style: any) => {
- return typeof style === 'function'
- ? [style()]
- : Array.isArray(style)
- ? style.map(style => typeof style === 'function' ? style() : style)
- : [style]
-}
-
-const updateStyles = ({ ref, style, state, scopedTheme, variants }: UpdateStylesProps) => {
- const styleResult = typeof style === 'function'
- ? style(state)
- : style
- const extractedResult = extractStyleResult(styleResult)
- const previousScopedTheme = UnistylesShadowRegistry.getScopedTheme()
- const previousVariants = UnistylesShadowRegistry.getVariants()
-
- UnistylesShadowRegistry.selectVariants(variants as unknown as Variants)
- UnistylesShadowRegistry.setScopedTheme(scopedTheme as any)
-
- UnistylesShadowRegistry.add(ref, extractedResult)
-
- UnistylesShadowRegistry.setScopedTheme(previousScopedTheme)
- UnistylesShadowRegistry.selectVariants(previousVariants as unknown as Variants)
-}
-
-export const Pressable = forwardRef(({ style, ...props }, passedRef) => {
- const storedRef = useRef(null)
+export const Pressable = forwardRef(({ style, ...props }, forwardedRef) => {
const scopedTheme = UnistylesShadowRegistry.getScopedTheme()
const variants = UnistylesShadowRegistry.getVariants()
+ let storedRef: HTMLElement | null = null
+ let classNames: ReturnType | undefined = undefined
return (
{
- if (!storedRef.current) {
- return {}
- }
+ ref={isServer() ? undefined : ref => {
+ storedRef = ref as unknown as HTMLElement
+ // @ts-expect-error hidden from TS
+ UnistylesShadowRegistry.add(storedRef, classNames?.hash)
- updateStyles({
- ref: storedRef.current,
- style: style as WebPressableStyle,
- variants,
- scopedTheme,
- state: state as WebPressableState
- })
+ if (typeof forwardedRef === 'function') {
+ return forwardedRef(ref)
+ }
- return {}
+ if (forwardedRef) {
+ forwardedRef.current = ref
+ }
}}
- ref={ref => {
- storedRef.current = ref
- updateStyles({
- ref,
- style: style as WebPressableStyle,
- variants,
- scopedTheme,
- state: initialState
- })
+ style={state => {
+ const styleResult = typeof style === 'function'
+ ? style(state as WebPressableState)
+ : style
+ const previousScopedTheme = UnistylesShadowRegistry.getScopedTheme()
+ const previousVariants = UnistylesShadowRegistry.getVariants()
- if (typeof passedRef === 'object' && passedRef !== null) {
- passedRef.current = ref
- }
+ UnistylesShadowRegistry.selectVariants(variants)
+ UnistylesShadowRegistry.setScopedTheme(scopedTheme)
+
+ // @ts-expect-error hidden from TS
+ UnistylesShadowRegistry.remove(storedRef, classNames?.hash)
+ classNames = getClassName(styleResult as UnistylesValues)
+ // @ts-expect-error hidden from TS
+ UnistylesShadowRegistry.add(storedRef, classNames?.hash)
+
+ UnistylesShadowRegistry.selectVariants(previousVariants)
+ UnistylesShadowRegistry.setScopedTheme(previousScopedTheme)
+
+ return classNames as any
}}
/>
)
diff --git a/src/core/createUnistylesElement.native.tsx b/src/core/createUnistylesElement.native.tsx
new file mode 100644
index 00000000..4dc4c2b1
--- /dev/null
+++ b/src/core/createUnistylesElement.native.tsx
@@ -0,0 +1,9 @@
+import React from 'react'
+import { passForwardedRef } from './passForwardRef'
+
+export const createUnistylesElement = (Component: any) => React.forwardRef((props, forwardedRef) => (
+ passForwardedRef(props, ref, forwardedRef)}
+ />
+))
diff --git a/src/core/createUnistylesElement.tsx b/src/core/createUnistylesElement.tsx
index 4dc4c2b1..71e5d23a 100644
--- a/src/core/createUnistylesElement.tsx
+++ b/src/core/createUnistylesElement.tsx
@@ -1,9 +1,39 @@
import React from 'react'
-import { passForwardedRef } from './passForwardRef'
-
-export const createUnistylesElement = (Component: any) => React.forwardRef((props, forwardedRef) => (
- passForwardedRef(props, ref, forwardedRef)}
- />
-))
+import type { UnistylesValues } from '../types'
+import { getClassName } from './getClassname'
+import { isServer } from '../web/utils'
+import { UnistylesShadowRegistry } from '../web'
+
+type ComponentProps = {
+ style?: UnistylesValues | Array
+}
+
+export const createUnistylesElement = (Component: any) => React.forwardRef((props, forwardedRef) => {
+ let storedRef: HTMLElement | null = null
+ const classNames = getClassName(props.style)
+
+ return (
+ {
+ if (!ref) {
+ // @ts-expect-error hidden from TS
+ UnistylesShadowRegistry.remove(storedRef, classNames?.hash)
+ }
+
+ storedRef = ref
+ // @ts-expect-error hidden from TS
+ UnistylesShadowRegistry.add(ref, classNames?.hash)
+
+ if (typeof forwardedRef === 'function') {
+ return forwardedRef(ref)
+ }
+
+ if (forwardedRef) {
+ forwardedRef.current = ref
+ }
+ }}
+ />
+ )
+})
diff --git a/src/core/createUnistylesImageBackground.tsx b/src/core/createUnistylesImageBackground.tsx
index 89bf2a69..1476688d 100644
--- a/src/core/createUnistylesImageBackground.tsx
+++ b/src/core/createUnistylesImageBackground.tsx
@@ -12,11 +12,11 @@ export const createUnistylesImageBackground = (Component: typeof ImageBackground
? props.imageStyle
: [props.imageStyle]
- // @ts-expect-error - This is hidden from TS
+ // @ts-expect-error web types are not compatible with RN styles
UnistylesShadowRegistry.add(ref, style)
return () => {
- // @ts-expect-error - This is hidden from TS
+ // @ts-expect-error hidden from TS
UnistylesShadowRegistry.remove(ref)
}
}}
diff --git a/src/core/getClassname.ts b/src/core/getClassname.ts
new file mode 100644
index 00000000..aff7821a
--- /dev/null
+++ b/src/core/getClassname.ts
@@ -0,0 +1,17 @@
+import type { UnistylesValues } from '../types';
+import { deepMergeObjects } from '../utils';
+import { UnistylesShadowRegistry } from '../web';
+
+export const getClassName = (unistyle: UnistylesValues | undefined | Array) => {
+ if (!unistyle) {
+ return undefined
+ }
+
+ const style = Array.isArray(unistyle)
+ ? deepMergeObjects(...unistyle)
+ : unistyle
+ // @ts-expect-error hidden from TS
+ const { hash, injectedClassName } = UnistylesShadowRegistry.addStyles(style)
+
+ return hash ? { $$css: true, hash, injectedClassName } : undefined
+}
diff --git a/src/core/index.ts b/src/core/index.ts
index 5edc11b0..82cb767f 100644
--- a/src/core/index.ts
+++ b/src/core/index.ts
@@ -3,3 +3,4 @@ export { createUnistylesElement } from './createUnistylesElement'
export { createUnistylesImageBackground } from './createUnistylesImageBackground'
export { withUnistyles } from './withUnistyles'
export { passForwardedRef } from './passForwardRef'
+export { getClassName } from './getClassname'
diff --git a/src/core/passForwardRef.ts b/src/core/passForwardRef.ts
index 3ea8071b..07b800a8 100644
--- a/src/core/passForwardRef.ts
+++ b/src/core/passForwardRef.ts
@@ -19,10 +19,11 @@ export const passForwardedRef = (
}
const forwardedRefReturnFn = passForwardedRef()
+ // @ts-expect-error hidden from TS
UnistylesShadowRegistry.add(ref, props.style)
return () => {
- // @ts-expect-error - This is hidden from TS
+ // @ts-expect-error hidden from TS
UnistylesShadowRegistry.remove(ref)
forwardedRefReturnFn?.()
}
diff --git a/src/core/withUnistyles/withUnistyles.tsx b/src/core/withUnistyles/withUnistyles.tsx
index 2b45c788..beca0448 100644
--- a/src/core/withUnistyles/withUnistyles.tsx
+++ b/src/core/withUnistyles/withUnistyles.tsx
@@ -1,42 +1,12 @@
-import React, { useEffect, useState, type ComponentType, forwardRef, useRef, useMemo, type ComponentProps, type ComponentRef } from 'react'
+import React, { type ComponentType, forwardRef, type ComponentProps, type ComponentRef } from 'react'
import type { PartialBy } from '../../types/common'
-import { UnistylesListener } from '../../web/listener'
-import { UnistylesShadowRegistry } from '../../web'
-import { equal } from '../../web/utils'
import { deepMergeObjects } from '../../utils'
import type { Mappings, SupportedStyleProps } from './types'
import { useDependencies } from './useDependencies'
import { UnistyleDependency } from '../../specs/NativePlatform'
import type { UnistylesValues } from '../../types'
-
-const useShadowRegistry = (style?: Record) => {
- const [ref] = useState(document.createElement('div'))
- const oldClassNames = useRef>([])
- const classNames = useMemo(() => {
- if (!style) {
- return []
- }
-
- const newClassNames = UnistylesShadowRegistry.add(ref, [style]) ?? []
-
- if (equal(oldClassNames.current, newClassNames)) {
- return oldClassNames.current
- }
-
- oldClassNames.current = newClassNames
-
- return newClassNames
- }, [style])
-
- useEffect(() => () => {
- // Remove styles on unmount
- if (style) {
- UnistylesShadowRegistry.add(null, [style])
- }
- })
-
- return classNames
-}
+import { getClassName } from '../getClassname'
+import { UnistylesWeb } from '../../web'
// @ts-expect-error
type GenericComponentProps = ComponentProps
@@ -53,11 +23,11 @@ export const withUnistyles = , PropsWithUnistyles>((props, ref) => {
const narrowedProps = props as PropsWithUnistyles
- const styleClassNames = useShadowRegistry(narrowedProps.style)
- const contentContainerStyleClassNames = useShadowRegistry(narrowedProps.contentContainerStyle)
+ const styleClassNames = getClassName(narrowedProps.style)
+ const contentContainerStyleClassNames = getClassName(narrowedProps.contentContainerStyle)
const { mappingsCallback } = useDependencies(({ dependencies, updateTheme, updateRuntime }) => {
- const disposeTheme = UnistylesListener.addListeners(dependencies.filter(dependency => dependency === UnistyleDependency.Theme), updateTheme)
- const disposeRuntime = UnistylesListener.addListeners(dependencies.filter(dependency => dependency !== UnistyleDependency.Theme), updateRuntime)
+ const disposeTheme = UnistylesWeb.listener.addListeners(dependencies.filter(dependency => dependency === UnistyleDependency.Theme), updateTheme)
+ const disposeRuntime = UnistylesWeb.listener.addListeners(dependencies.filter(dependency => dependency !== UnistyleDependency.Theme), updateRuntime)
return () => {
disposeTheme()
@@ -70,16 +40,10 @@ export const withUnistyles =
+ }
+}
+
+export const useServerUnistyles = () => {
+ const isServerInserted = useRef(false)
+
+ useServerInsertedHTML(() => {
+ if (!isServerInserted.current) {
+ isServerInserted.current = true
+
+ // @ts-ignore
+ const rnwStyle = StyleSheet?.getSheet().textContent ?? ''
+ const css = UnistylesWeb.registry.css.getStyles()
+ const state = UnistylesWeb.registry.css.getState()
+ UnistylesWeb.registry.reset()
+
+ return (
+ <>
+
+
+
+ >
+ )
+ }
+
+ return null
+ })
+
+ if (typeof window !== 'undefined') {
+ UnistylesWeb.registry.css.hydrate(window.__UNISTYLES_STATE__)
+ }
+}
diff --git a/src/utils.ts b/src/utils.ts
index 34e87d70..fde9dd34 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -1,20 +1,24 @@
+export const isDefined = (value: T): value is NonNullable => value !== undefined && value !== null
+
export const deepMergeObjects = >(...sources: Array) => {
const target = {} as T
- sources.forEach(source => {
- Object.keys(source).forEach(key => {
- const sourceValue = source[key]
- const targetValue = target[key]
+ sources
+ .filter(isDefined)
+ .forEach(source => {
+ Object.keys(source).forEach(key => {
+ const sourceValue = source[key]
+ const targetValue = target[key]
- if (Object(sourceValue) === sourceValue && Object(targetValue) === targetValue) {
- // @ts-expect-error - can't assign to generic
- target[key] = deepMergeObjects(targetValue, sourceValue)
+ if (Object(sourceValue) === sourceValue && Object(targetValue) === targetValue) {
+ // @ts-expect-error - can't assign to generic
+ target[key] = deepMergeObjects(targetValue, sourceValue)
- return
- }
+ return
+ }
- // @ts-expect-error - can't assign to generic
- target[key] = sourceValue
+ // @ts-expect-error - can't assign to generic
+ target[key] = sourceValue
})
})
diff --git a/src/web/convert/object/filter.ts b/src/web/convert/object/filter.ts
index 3858da2f..89046a97 100644
--- a/src/web/convert/object/filter.ts
+++ b/src/web/convert/object/filter.ts
@@ -3,8 +3,8 @@ import { hyphenate } from '../../utils'
import type { Filters } from '../types'
import { getObjectStyle } from './objectStyle'
import { normalizeColor, normalizeNumericValue } from '../utils'
-import { UnistylesRuntime } from '../../runtime'
import { isUnistylesMq } from '../../../mq'
+import { UnistylesWeb } from '../../index'
const getDropShadowStyle = (dropShadow: DropShadowValue) => {
const { offsetX = 0, offsetY = 0, standardDeviation = 0, color = '#000' } = dropShadow
@@ -21,7 +21,7 @@ export const getFilterStyle = (filters: Array) => {
return []
}
- const breakpoints = Object.keys(dropShadowValue).filter(key => Object.keys(UnistylesRuntime.breakpoints).includes(key) || isUnistylesMq(key))
+ const breakpoints = Object.keys(dropShadowValue).filter(key => Object.keys(UnistylesWeb.runtime.breakpoints).includes(key) || isUnistylesMq(key))
const breakpointsDropShadow = Object.fromEntries(breakpoints.map(breakpoint => [breakpoint, getDropShadowStyle(dropShadowValue[breakpoint])]))
if (breakpoints.length === 0) {
diff --git a/src/web/convert/style.ts b/src/web/convert/style.ts
index 60f16bfa..eb6f8f9a 100644
--- a/src/web/convert/style.ts
+++ b/src/web/convert/style.ts
@@ -1,15 +1,15 @@
import { keyInObject } from '../utils'
-const SKIP_STYLES = [
+const SKIP_STYLES = new Set([
'borderCurve',
'elevation',
'textAlignVertical',
'includeFontPadding',
'overlayColor',
'tintColor'
-]
+])
-const CSS_NUMBER_KEYS = [
+const CSS_NUMBER_KEYS = new Set([
'animationIterationCount',
'borderImageOutset',
'borderImageSlice',
@@ -54,7 +54,7 @@ const CSS_NUMBER_KEYS = [
'strokeMiterlimit',
'strokeOpacity',
'strokeWidth'
-]
+])
const convertMap = {
marginHorizontal: (value: number) => ({
@@ -124,14 +124,14 @@ const convertMap = {
const convertNumber = (key: string, value: any) => {
if (typeof value === 'number') {
- return CSS_NUMBER_KEYS.includes(key, value) ? value : `${value}px`
+ return CSS_NUMBER_KEYS.has(key) ? value : `${value}px`
}
return value
}
export const getStyle = (key: string, value: any) => {
- if (SKIP_STYLES.includes(key)) {
+ if (SKIP_STYLES.has(key)) {
return {}
}
diff --git a/src/web/create.ts b/src/web/create.ts
index 102ff4ba..46b56fc0 100644
--- a/src/web/create.ts
+++ b/src/web/create.ts
@@ -1,6 +1,6 @@
import type { StyleSheetWithSuperPowers, StyleSheet } from '../types/stylesheet'
+import { UnistylesWeb } from './index'
import { assignSecrets, error, removeInlineStyles } from './utils'
-import { UnistylesRuntime } from './runtime'
const useVariants = ['useVariants', () => {}]
@@ -10,14 +10,13 @@ export const create = (stylesheet: StyleSheetWithSuperPowers, id?: s
}
const computedStylesheet = typeof stylesheet === 'function'
- ? stylesheet(UnistylesRuntime.theme, UnistylesRuntime.miniRuntime)
+ ? stylesheet(UnistylesWeb.runtime.theme, UnistylesWeb.runtime.miniRuntime)
: stylesheet
const addSecrets = (value: any, key: string, args?: Array) => assignSecrets(value, {
__uni__key: key,
__uni__stylesheet: stylesheet,
- __uni__args: args,
- __uni__refs: new Set()
+ __uni__args: args
})
const styleSheetStyles = Object.entries(computedStylesheet).map(([key, value]) => {
diff --git a/src/web/css/core.ts b/src/web/css/core.ts
new file mode 100644
index 00000000..7fc364f4
--- /dev/null
+++ b/src/web/css/core.ts
@@ -0,0 +1,58 @@
+import { getMediaQuery } from '../utils'
+import type { CSSState } from './state'
+
+export const convertToCSS = (hash: string, value: Record, state: CSSState) => {
+ Object.entries(value).forEach(([styleKey, styleValue]) => {
+ if (styleKey[0] === '_') {
+ const pseudoClassName = styleKey.replace('_', `${hash}:`)
+
+ Object.entries(styleValue).forEach(([pseudoStyleKey, pseudoStyleValue]) => {
+ if (typeof pseudoStyleValue === 'object' && pseudoStyleValue !== null) {
+ const allBreakpoints = Object.keys(pseudoStyleValue)
+ Object.entries(pseudoStyleValue).forEach(([breakpointStyleKey, breakpointStyleValue]) => {
+ const mediaQuery = getMediaQuery(pseudoStyleKey, allBreakpoints)
+
+ state.set({
+ mediaQuery,
+ className: pseudoClassName,
+ propertyKey: breakpointStyleKey,
+ value: breakpointStyleValue,
+ })
+ })
+
+ return
+ }
+
+ state.set({
+ className: pseudoClassName,
+ propertyKey: pseudoStyleKey,
+ value: pseudoStyleValue,
+ })
+ })
+
+ return
+ }
+
+ if (typeof styleValue === 'object') {
+ const allBreakpoints = Object.keys(styleValue)
+ Object.entries(styleValue).forEach(([breakpointStyleKey, breakpointStyleValue]) => {
+ const mediaQuery = getMediaQuery(styleKey, allBreakpoints)
+
+ state.set({
+ mediaQuery,
+ className: hash,
+ propertyKey: breakpointStyleKey,
+ value: breakpointStyleValue,
+ })
+ })
+
+ return
+ }
+
+ state.set({
+ className: hash,
+ propertyKey: styleKey,
+ value: styleValue,
+ })
+ })
+}
diff --git a/src/web/css/index.ts b/src/web/css/index.ts
new file mode 100644
index 00000000..255e080b
--- /dev/null
+++ b/src/web/css/index.ts
@@ -0,0 +1 @@
+export * from './state'
diff --git a/src/web/css/state.ts b/src/web/css/state.ts
new file mode 100644
index 00000000..08ac7759
--- /dev/null
+++ b/src/web/css/state.ts
@@ -0,0 +1,165 @@
+import type { UnistylesValues } from '../../types'
+import { convertUnistyles } from '../convert'
+import { hyphenate, isServer } from '../utils'
+import { convertToCSS } from './core'
+
+type MapType = Map>>
+type SetProps = {
+ mediaQuery?: string
+ className: string
+ isMq?: boolean
+ propertyKey: string
+ value: any
+}
+type HydrateState = Array<[ string, Array<[ string, Array<[ string, any ]> ]> ]>
+
+const safeGetMap = (map: Map>, key: string) => {
+ const nextLevelMap = map.get(key)
+
+ if (!nextLevelMap) {
+ const newMap = new Map()
+
+ map.set(key, newMap)
+
+ return newMap
+ }
+
+ return nextLevelMap
+}
+
+export class CSSState {
+ mainMap: MapType = new Map()
+ mqMap: MapType = new Map()
+ private styleTag: HTMLStyleElement | null = null
+
+ constructor() {
+ if (isServer()) {
+ return
+ }
+
+ const ssrTag = document.getElementById('unistyles-web')
+
+ if (ssrTag) {
+ this.styleTag = ssrTag as HTMLStyleElement
+
+ return
+ }
+
+ this.styleTag = document.createElement('style')
+ this.styleTag.id = 'unistyles-web'
+ document.head.appendChild(this.styleTag)
+ }
+
+ set = ({ className, propertyKey, value, mediaQuery = '', isMq }: SetProps) => {
+ const firstLevelMap = isMq ? this.mqMap : this.mainMap
+ const secondLevelMap = safeGetMap(firstLevelMap, mediaQuery)
+ const thirdLevelMap = safeGetMap(secondLevelMap, className)
+
+ thirdLevelMap.set(propertyKey, value)
+ }
+
+ add = (hash: string, values: UnistylesValues) => {
+ convertToCSS(hash, convertUnistyles(values), this)
+
+ if (this.styleTag) {
+ this.styleTag.innerText = this.getStyles()
+ }
+ }
+
+ remove = (hash: string) => {
+ this.mainMap.forEach(styles => {
+ styles.delete(hash)
+ })
+
+ if (this.styleTag) {
+ this.styleTag.innerText = this.getStyles()
+ }
+ }
+
+ getStyles = () => {
+ let styles = ''
+
+ const generate = (mediaQuery: string, secondLevelMap: Map>) => {
+ if (mediaQuery) {
+ styles += `${mediaQuery}{`
+ }
+
+ for (const [className, thirdLevelMap] of secondLevelMap) {
+ styles += `.${className}{`
+
+ for (const [propertyKey, value] of thirdLevelMap) {
+ if (value === undefined) {
+ continue
+ }
+
+ styles += `${hyphenate(propertyKey)}:${value};`
+ }
+
+ styles += '}'
+ }
+
+ if (mediaQuery) {
+ styles += '}'
+ }
+ }
+
+ for (const [mediaQuery, secondLevelMap] of this.mainMap) {
+ generate(mediaQuery, secondLevelMap)
+ }
+
+ for (const [mediaQuery, secondLevelMap] of this.mqMap) {
+ generate(mediaQuery, secondLevelMap)
+ }
+
+ return styles
+ }
+
+ getState = () => {
+ const getState = (map: MapType) => {
+ return Array.from(map).map(([mediaQuery, classNames]) => {
+ return [
+ mediaQuery,
+ Array.from(classNames.entries()).map(([className, style]) => {
+ return [
+ className,
+ Array.from(style.entries()).map(([property, value]) => {
+ return [property, value]
+ })
+ ]
+ })
+ ]
+ }) as HydrateState
+ }
+
+ const mainState = getState(this.mainMap)
+ const mqState = getState(this.mqMap)
+
+ return { mainState, mqState }
+ }
+
+ hydrate = ({ mainState, mqState }: ReturnType) => {
+ const hydrateState = (map: HydrateState, isMq = false) => {
+ map.forEach(([mediaQuery, classNames]) => {
+ classNames.forEach(([className, style]) => {
+ style.forEach(([propertyKey, value]) => {
+ this.set({
+ className,
+ propertyKey,
+ value,
+ mediaQuery,
+ isMq
+ })
+ })
+ })
+ })
+ }
+
+ hydrateState(mainState)
+ hydrateState(mqState, true)
+ }
+
+ reset = () => {
+ this.mqMap.clear()
+ this.mainMap.clear()
+ }
+}
diff --git a/src/web/css/utils.ts b/src/web/css/utils.ts
new file mode 100644
index 00000000..55ca5629
--- /dev/null
+++ b/src/web/css/utils.ts
@@ -0,0 +1,54 @@
+import { hyphenate } from '../utils'
+import type { CSSState } from './state'
+
+export const safeGetMap = (map: Map>, key: string) => {
+ const nextLevelMap = map.get(key)
+
+ if (!nextLevelMap) {
+ const newMap = new Map()
+
+ map.set(key, newMap)
+
+ return newMap
+ }
+
+ return nextLevelMap
+}
+
+export const getStylesFromState = (state: CSSState) => {
+ let styles = ''
+
+ const generate = (mediaQuery: string, secondLevelMap: Map>) => {
+ if (mediaQuery) {
+ styles += `${mediaQuery}{`
+ }
+
+ for (const [className, thirdLevelMap] of secondLevelMap) {
+ styles += `.${className}{`
+
+ for (const [propertyKey, value] of thirdLevelMap) {
+ if (value === undefined) {
+ continue
+ }
+
+ styles += `${hyphenate(propertyKey)}:${value};`
+ }
+
+ styles += '}'
+ }
+
+ if (mediaQuery) {
+ styles += '}'
+ }
+ }
+
+ for (const [mediaQuery, secondLevelMap] of state.mainMap) {
+ generate(mediaQuery, secondLevelMap)
+ }
+
+ for (const [mediaQuery, secondLevelMap] of state.mqMap) {
+ generate(mediaQuery, secondLevelMap)
+ }
+
+ return styles
+}
diff --git a/src/web/index.ts b/src/web/index.ts
index 28607f36..76482fb1 100644
--- a/src/web/index.ts
+++ b/src/web/index.ts
@@ -1,12 +1,24 @@
import { create } from './create'
-import { UnistylesState } from './state'
import { deepMergeObjects } from '../utils'
import type { StyleSheet as NativeStyleSheet } from '../specs/StyleSheet'
-import { UnistylesRuntime as UnistylesRuntimeWeb } from './runtime'
import type { Runtime as NativeUnistylesRuntime } from '../specs/UnistylesRuntime'
+import type { UnistylesShadowRegistry as NativeUnistylesShadowRegistry } from '../specs/ShadowRegistry'
+import { UnistylesServices } from './services'
+import { isServer } from './utils'
+declare global {
+ // @ts-ignore
+ var __unistyles__: UnistylesServices
+}
+
+if (isServer() && !globalThis.__unistyles__) {
+ // @ts-ignore
+ globalThis.__unistyles__ = new UnistylesServices()
+}
+
+export const UnistylesWeb = isServer() ? globalThis.__unistyles__ : new UnistylesServices()
export const StyleSheet = {
- configure: UnistylesState.init,
+ configure: UnistylesWeb.state.init,
create: create,
absoluteFill: {
position: 'absolute',
@@ -27,7 +39,8 @@ export const StyleSheet = {
hairlineWidth: 1
} as unknown as typeof NativeStyleSheet
-export const UnistylesRuntime = UnistylesRuntimeWeb as unknown as typeof NativeUnistylesRuntime
+export const UnistylesRuntime = UnistylesWeb.runtime as unknown as typeof NativeUnistylesRuntime
+export const UnistylesShadowRegistry = UnistylesWeb.shadowRegistry as unknown as typeof NativeUnistylesShadowRegistry
export * from './mock'
-export * from './shadowRegistry'
+
diff --git a/src/web/listener.ts b/src/web/listener.ts
index 691ed0c5..6757851c 100644
--- a/src/web/listener.ts
+++ b/src/web/listener.ts
@@ -1,11 +1,13 @@
import { UnistyleDependency } from '../specs/NativePlatform'
-import { UnistylesRuntime } from './runtime'
+import type { UnistylesServices } from './types'
-class UnistylesListenerBuilder {
+export class UnistylesListener {
private isInitialized = false
private listeners = Array.from({ length: Object.keys(UnistyleDependency).length / 2 }, () => new Set())
private stylesheetListeners = Array.from({ length: Object.keys(UnistyleDependency).length / 2 }, () => new Set())
+ constructor(private services: UnistylesServices) {}
+
emitChange = (dependency: UnistyleDependency) => {
this.stylesheetListeners[dependency]?.forEach(listener => listener())
this.listeners[dependency]?.forEach(listener => listener())
@@ -18,25 +20,25 @@ class UnistylesListenerBuilder {
this.isInitialized = true
- UnistylesRuntime.darkMedia?.addEventListener('change', event => {
+ this.services.runtime.darkMedia?.addEventListener('change', event => {
if (!event.matches) {
return
}
this.emitChange(UnistyleDependency.ColorScheme)
- if (UnistylesRuntime.hasAdaptiveThemes) {
+ if (this.services.runtime.hasAdaptiveThemes) {
this.emitChange(UnistyleDependency.Theme)
}
})
- UnistylesRuntime.lightMedia?.addEventListener('change', event => {
+ this.services.runtime.lightMedia?.addEventListener('change', event => {
if (!event.matches) {
return
}
this.emitChange(UnistyleDependency.ColorScheme)
- if (UnistylesRuntime.hasAdaptiveThemes) {
+ if (this.services.runtime.hasAdaptiveThemes) {
this.emitChange(UnistyleDependency.Theme)
}
})
@@ -61,5 +63,3 @@ class UnistylesListenerBuilder {
}
}
}
-
-export const UnistylesListener = new UnistylesListenerBuilder()
diff --git a/src/web/registry.ts b/src/web/registry.ts
index 819fb007..38a36c94 100644
--- a/src/web/registry.ts
+++ b/src/web/registry.ts
@@ -1,42 +1,19 @@
import type { UnistylesTheme, UnistylesValues } from '../types'
import type { StyleSheet, StyleSheetWithSuperPowers } from '../types/stylesheet'
-import { UnistylesRuntime } from './runtime'
-import { extractMediaQueryValue, keyInObject, getMediaQuery, generateHash, extractUnistyleDependencies, error } from './utils'
-import { UnistylesListener } from './listener'
-import { convertUnistyles } from './convert'
+import { generateHash, extractUnistyleDependencies, error } from './utils'
import type { UnistylesMiniRuntime, UnistyleDependency } from '../specs'
+import { CSSState } from './css'
+import type { UnistylesServices } from './types'
-type ApplyRuleProps = {
- hash: string,
- key: string,
- value: any,
- sheet: CSSStyleSheet | CSSMediaRule
-}
-
-type RemoveReadonlyStyleKeys = T extends 'length' | 'parentRule' ? never : T
-
-class UnistylesRegistryBuilder {
+export class UnistylesRegistry {
private readonly stylesheets = new Map, StyleSheet>()
- private readonly stylesCounter = new Map>()
- #styleTag: HTMLStyleElement | null = null
+ private readonly stylesCache = new Set()
+ private readonly stylesCounter = new Map>()
private readonly disposeListenersMap = new Map