From 1976ac0097d956f4465647042f299f8c60627028 Mon Sep 17 00:00:00 2001 From: Hubert Bieszczad Date: Wed, 11 Dec 2024 13:54:05 +0100 Subject: [PATCH 01/19] feat: static ssr --- src/components/native/Pressable.native.tsx | 4 - src/components/native/Pressable.tsx | 22 ++- src/core/createUnistylesElement.native.tsx | 9 ++ src/core/createUnistylesElement.tsx | 30 +++- src/core/createUnistylesImageBackground.tsx | 2 - src/core/passForwardRef.ts | 1 - src/web/convert/style.ts | 12 +- src/web/create.ts | 5 +- src/web/css/core.ts | 58 +++++++ src/web/css/index.ts | 1 + src/web/css/state.ts | 82 ++++++++++ src/web/css/utils.ts | 54 +++++++ src/web/registry.ts | 167 ++++---------------- src/web/shadowRegistry.ts | 156 +++++------------- src/web/utils/common.ts | 38 ++++- src/web/utils/unistyle.ts | 32 ++-- 16 files changed, 352 insertions(+), 321 deletions(-) create mode 100644 src/core/createUnistylesElement.native.tsx create mode 100644 src/web/css/core.ts create mode 100644 src/web/css/index.ts create mode 100644 src/web/css/state.ts create mode 100644 src/web/css/utils.ts diff --git a/src/components/native/Pressable.native.tsx b/src/components/native/Pressable.native.tsx index 5a8f342e..eaf05dbe 100644 --- a/src/components/native/Pressable.native.tsx +++ b/src/components/native/Pressable.native.tsx @@ -28,11 +28,9 @@ export const Pressable = forwardRef(({ variants, style, .. ? style({ pressed: false }) : style - // @ts-expect-error - this is hidden from TS UnistylesShadowRegistry.add(ref, [unistyles]) return () => { - // @ts-expect-error - this is hidden from TS UnistylesShadowRegistry.remove(ref) if (typeof returnFn === 'function') { @@ -49,10 +47,8 @@ export const Pressable = forwardRef(({ variants, style, .. : [unistyles] if (storedRef.current) { - // @ts-expect-error - this is hidden from TS UnistylesShadowRegistry.remove(storedRef.current) UnistylesShadowRegistry.selectVariants(variants) - // @ts-expect-error - this is hidden from TS UnistylesShadowRegistry.add(storedRef.current, styles) UnistylesShadowRegistry.selectVariants(undefined) } diff --git a/src/components/native/Pressable.tsx b/src/components/native/Pressable.tsx index 5bff2fbe..02f6d2b9 100644 --- a/src/components/native/Pressable.tsx +++ b/src/components/native/Pressable.tsx @@ -1,7 +1,8 @@ import React, { forwardRef, useEffect, useRef } 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' type Variants = Record type WebPressableState = { @@ -10,7 +11,7 @@ type WebPressableState = { focused: boolean } -type WebPressableStyle = ((state: WebPressableState) => ViewStyle) | ViewStyle +type WebPressableStyle = ((state: WebPressableState) => UnistylesValues) | UnistylesValues type PressableProps = Props & { variants?: Variants @@ -40,26 +41,23 @@ type UpdateStylesProps = { 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) + const { hash, injectedClassName } = UnistylesShadowRegistry.addStyles(styleResult) + + const pressableRef = (ref as HTMLDivElement | null) + + pressableRef?.classList.remove(...Array.from(pressableRef.classList)) + pressableRef?.classList.add(hash) + pressableRef?.classList.add(injectedClassName) UnistylesShadowRegistry.setScopedTheme(previousScopedTheme) UnistylesShadowRegistry.selectVariants(previousVariants as unknown as Variants) 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..66341b73 100644 --- a/src/core/createUnistylesElement.tsx +++ b/src/core/createUnistylesElement.tsx @@ -1,9 +1,25 @@ import React from 'react' -import { passForwardedRef } from './passForwardRef' +import type { UnistylesValues } from '../types' +import { UnistylesShadowRegistry } from '../web' +import { deepMergeObjects } from '../utils' -export const createUnistylesElement = (Component: any) => React.forwardRef((props, forwardedRef) => ( - passForwardedRef(props, ref, forwardedRef)} - /> -)) +type ComponentProps = { + style?: UnistylesValues | Array +} + +export const createUnistylesElement = (Component: any) => React.forwardRef((props, forwardedRef) => { + const style = Array.isArray(props.style) + ? deepMergeObjects(...props.style) + : props.style + const { hash, injectedClassName } = style + ? UnistylesShadowRegistry.addStyles(style) + : {} + + return ( + + ) +}) diff --git a/src/core/createUnistylesImageBackground.tsx b/src/core/createUnistylesImageBackground.tsx index 89bf2a69..d81bb036 100644 --- a/src/core/createUnistylesImageBackground.tsx +++ b/src/core/createUnistylesImageBackground.tsx @@ -12,11 +12,9 @@ export const createUnistylesImageBackground = (Component: typeof ImageBackground ? props.imageStyle : [props.imageStyle] - // @ts-expect-error - This is hidden from TS UnistylesShadowRegistry.add(ref, style) return () => { - // @ts-expect-error - This is hidden from TS UnistylesShadowRegistry.remove(ref) } }} diff --git a/src/core/passForwardRef.ts b/src/core/passForwardRef.ts index 3ea8071b..ba29cef3 100644 --- a/src/core/passForwardRef.ts +++ b/src/core/passForwardRef.ts @@ -22,7 +22,6 @@ export const passForwardedRef = ( UnistylesShadowRegistry.add(ref, props.style) return () => { - // @ts-expect-error - This is hidden from TS UnistylesShadowRegistry.remove(ref) forwardedRefReturnFn?.() } 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 97675de8..102ff4ba 100644 --- a/src/web/create.ts +++ b/src/web/create.ts @@ -1,11 +1,8 @@ import type { StyleSheetWithSuperPowers, StyleSheet } from '../types/stylesheet' import { assignSecrets, error, removeInlineStyles } from './utils' import { UnistylesRuntime } from './runtime' -import { UnistylesShadowRegistry } from '../specs' -const useVariants = ['useVariants', (variants: Record) => { - UnistylesShadowRegistry.selectVariants(variants) -}] +const useVariants = ['useVariants', () => {}] export const create = (stylesheet: StyleSheetWithSuperPowers, id?: string) => { if (!id) { 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..1cb9b4d4 --- /dev/null +++ b/src/web/css/state.ts @@ -0,0 +1,82 @@ +import type { UnistylesValues } from '../../types' +import { convertUnistyles } from '../convert' +import { hyphenate } from '../utils' +import { convertToCSS } from './core' + +type MapType = Map>> +type SetthisProps = { + mediaQuery?: string + className: string + isMq?: boolean + propertyKey: string + value: 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() + + set = ({ className, propertyKey, value, mediaQuery = '', isMq }: SetthisProps) => { + 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) + } + + 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 + } +} 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/registry.ts b/src/web/registry.ts index 819fb007..1a0c6596 100644 --- a/src/web/registry.ts +++ b/src/web/registry.ts @@ -1,41 +1,27 @@ 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 { generateHash, extractUnistyleDependencies, error, isServer } from './utils' import { UnistylesListener } from './listener' -import { convertUnistyles } from './convert' import type { UnistylesMiniRuntime, UnistyleDependency } from '../specs' - -type ApplyRuleProps = { - hash: string, - key: string, - value: any, - sheet: CSSStyleSheet | CSSMediaRule -} - -type RemoveReadonlyStyleKeys = T extends 'length' | 'parentRule' ? never : T +import { CSSState } from './css' class UnistylesRegistryBuilder { private readonly stylesheets = new Map, StyleSheet>() - private readonly stylesCounter = new Map>() - #styleTag: HTMLStyleElement | null = null + private readonly stylesCounter = new Map() private readonly disposeListenersMap = new Map() private readonly dependenciesMap = new Map, Set>() + readonly css = new CSSState() + private styleTag: HTMLStyleElement | null = null - private get styleTag() { - const tag = this.#styleTag - - if (!tag) { - const newTag = document.createElement('style') - - newTag.id = 'unistyles-web' - this.#styleTag = newTag - document.head.appendChild(newTag) - - return newTag + constructor() { + if (isServer()) { + return } - return tag + this.styleTag = document.createElement('style') + this.styleTag.id = 'unistyles-web' + document.head.appendChild(this.styleTag) } getComputedStylesheet = (stylesheet: StyleSheetWithSuperPowers, scopedThemeName?: UnistylesTheme) => { @@ -89,136 +75,37 @@ class UnistylesRegistryBuilder { const hash = generateHash(value) const existingCounter = this.stylesCounter.get(hash) - if (!existingCounter || existingCounter.size === 0) { - const counter = new Set() - - counter.add(value) - this.stylesCounter.set(hash, counter) + if (existingCounter === undefined) { this.applyStyles(hash, value) + this.stylesCounter.set(hash, 1) return { hash, existingHash: false } } - existingCounter.add(value) + this.stylesCounter.set(hash, existingCounter + 1) return { hash, existingHash: true } } - applyStyles = (hash: string, styles: Record) => { - Object.entries(convertUnistyles(styles)).forEach(([key, value]) => { - if (!this.styleTag.sheet) { - return - } - - if (typeof value === 'object' && !key.startsWith('_')) { - const mediaQuery = getMediaQuery(key) - const cssRules = Array.from(this.styleTag.sheet.cssRules) - let queryRule = cssRules.find(rule => { - if (!(rule instanceof CSSMediaRule)) { - return false - } - - return rule.media.item(0)?.includes(mediaQuery) - }) ?? null - - if (!queryRule) { - const mediaQueryValue = extractMediaQueryValue(mediaQuery) - const ruleIndex = mediaQueryValue - ? cssRules.reduce((acc, rule, ruleIndex) => { - if (!(rule instanceof CSSMediaRule)) { - return acc - } - - const ruleMediaQueryValue = extractMediaQueryValue(rule.conditionText) - - if (ruleMediaQueryValue === undefined) { - return - } - - return ruleMediaQueryValue > mediaQueryValue ? ruleIndex : acc - }, cssRules.length) - : undefined - queryRule = this.styleTag.sheet.cssRules.item(this.styleTag.sheet.insertRule(`@media ${mediaQuery} {.${hash} {}}`, ruleIndex)) - } - - if (queryRule instanceof CSSMediaRule) { - Object.entries(value).forEach(([mqKey, mqValue]) => { - this.applyRule({ - hash, - key: mqKey, - value: mqValue, - sheet: queryRule - }) - }) - } - - return - } - - // Pseudo - if (typeof value === 'object') { - Object.entries(value).forEach(([pseudoKey, pseudoValue]) => { - this.applyRule({ - hash: `${hash}${key.replace('_', ':')}`, - key: pseudoKey, - value: pseudoValue, - sheet: this.styleTag.sheet as CSSStyleSheet - }) - }) - - return - } - - this.applyRule({ - hash, - key, - value, - sheet: this.styleTag.sheet - }) - }) + remove = (hash: string) => { + hash } - private applyRule = ({ hash, key, value, sheet }: ApplyRuleProps) => { - let rule = Array.from(sheet.cssRules).find(rule => { - if (!(rule instanceof CSSStyleRule)) { - return false - } - - // In unistyles pseudos are prefixed with ':' but in css some of them are prefixed with '::' - return rule.selectorText.replace('::', ':').includes(hash) - }) ?? null + applyStyles = (hash: string, value: UnistylesValues) => { + this.css.add(hash, value) - if (!rule) { - rule = sheet.cssRules.item(sheet.insertRule(`.${hash} {}`)) + if (this.styleTag) { + this.styleTag.innerHTML = this.css.getStyles() } - - if (!(rule instanceof CSSStyleRule) || !keyInObject(rule.style, key)) { - return - } - - rule.style[key as RemoveReadonlyStyleKeys] = value } +} - remove = (value: UnistylesValues) => { - const hash = generateHash(value) - const existingStyles = this.stylesCounter.get(hash) - - if (!existingStyles) { - return - } - - existingStyles.delete(value) - - if (existingStyles.size === 0) { - const ruleIndex = Array.from(this.styleTag.sheet?.cssRules ?? []).findIndex(rule => rule.cssText.includes(`.${hash}`)) - - if (ruleIndex === -1) { - return - } +declare global { + var __unistyles__: UnistylesRegistryBuilder +} - this.styleTag.sheet?.deleteRule(ruleIndex) - } - } +if (isServer() && !globalThis.__unistyles__) { + globalThis.__unistyles__ = new UnistylesRegistryBuilder() } -export const UnistylesRegistry = new UnistylesRegistryBuilder() +export const UnistylesRegistry = isServer() ? globalThis.__unistyles__ : new UnistylesRegistryBuilder() diff --git a/src/web/shadowRegistry.ts b/src/web/shadowRegistry.ts index b47eae7b..38f9872c 100644 --- a/src/web/shadowRegistry.ts +++ b/src/web/shadowRegistry.ts @@ -2,11 +2,9 @@ import type { UnistylesTheme, UnistylesValues } from '../types' import { UnistylesListener } from './listener' import { UnistylesRegistry } from './registry' import { deepMergeObjects } from '../utils' -import { equal, extractSecrets, extractUnistyleDependencies, isInDocument, keyInObject } from './utils' +import { extractSecrets, extractUnistyleDependencies } from './utils' import { getVariants } from './variants' -type Style = UnistylesValues | ((...args: Array) => UnistylesValues) - class UnistylesShadowRegistryBuilder { // MOCKS name = 'UnistylesShadowRegistry' @@ -16,139 +14,57 @@ class UnistylesShadowRegistryBuilder { dispose = () => {} // END MOCKS - private resultsMap = new Map() - private hashMap = new Map() - private classNamesMap = new Map>() private selectedVariants = new Map() private scopedTheme: UnistylesTheme | undefined = undefined - add = (ref: any, styles: Array + + + ) + } + + return null + }) + + if (typeof window !== 'undefined') { + UnistylesWeb.registry.css.hydrate(window.UNISTYLES_STATE) + } +} diff --git a/src/specs/UnistylesRuntime/index.ts b/src/specs/UnistylesRuntime/index.ts index e10cef4c..13293d8f 100644 --- a/src/specs/UnistylesRuntime/index.ts +++ b/src/specs/UnistylesRuntime/index.ts @@ -24,8 +24,6 @@ export interface UnistylesRuntimePrivate extends Omit AppTheme): void, setRootViewBackgroundColor(color?: string): void, _setRootViewBackgroundColor(color?: Color): void - getCSS(): string, - resetStyles(): void // constructors createHybridStatusBar(): UnistylesStatusBar, diff --git a/src/web/convert/object/filter.ts b/src/web/convert/object/filter.ts index d9e1907e..f7eca354 100644 --- a/src/web/convert/object/filter.ts +++ b/src/web/convert/object/filter.ts @@ -4,7 +4,7 @@ import type { Filters } from '../types' import { getObjectStyle } from './objectStyle' import { normalizeColor, normalizeNumericValue } from '../utils' import { isUnistylesMq } from '../../../mq' -import { UnistylesWeb } from '../../services' +import { UnistylesWeb } from '../..' const getDropShadowStyle = (dropShadow: DropShadowValue) => { const { offsetX = 0, offsetY = 0, standardDeviation = 0, color = '#000' } = dropShadow diff --git a/src/web/create.ts b/src/web/create.ts index bb60aea2..d5c0d199 100644 --- a/src/web/create.ts +++ b/src/web/create.ts @@ -1,5 +1,5 @@ import type { StyleSheetWithSuperPowers, StyleSheet } from '../types/stylesheet' -import { UnistylesWeb } from './services' +import { UnistylesWeb } from '.' import { assignSecrets, error, removeInlineStyles } from './utils' const useVariants = ['useVariants', () => {}] diff --git a/src/web/css/state.ts b/src/web/css/state.ts index 5a75d035..076bcfa7 100644 --- a/src/web/css/state.ts +++ b/src/web/css/state.ts @@ -112,6 +112,37 @@ export class CSSState { return styles } + getState = () => { + return Array.from(this.mainMap.entries()).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 Array<[ string, Array<[ string, Array<[ string, any ]> ]> ]> + } + + hydrate = (state: ReturnType) => { + state.forEach(([mediaQuery, classNames]) => { + classNames.forEach(([className, style]) => { + style.forEach(([propertyKey, value]) => { + this.set({ + className, + propertyKey, + value, + mediaQuery + }) + }) + }) + }) + } + reset = () => { this.mqMap.clear() this.mainMap.clear() diff --git a/src/web/index.ts b/src/web/index.ts index 8832c8c7..d275b103 100644 --- a/src/web/index.ts +++ b/src/web/index.ts @@ -3,8 +3,18 @@ import { deepMergeObjects } from '../utils' import type { StyleSheet as NativeStyleSheet } from '../specs/StyleSheet' import type { Runtime as NativeUnistylesRuntime } from '../specs/UnistylesRuntime' import type { UnistylesShadowRegistry as NativeUnistylesShadowRegistry } from '../specs/ShadowRegistry' -import { UnistylesWeb } from './services' +import { UnistylesServices } from './services' +import { isServer } from './utils' +declare global { + var __unistyles__: UnistylesServices +} + +if (isServer() && !globalThis.__unistyles__) { + globalThis.__unistyles__ = new UnistylesServices() +} + +export const UnistylesWeb = isServer() ? globalThis.__unistyles_web__ : new UnistylesServices() export const StyleSheet = { configure: UnistylesWeb.state.init, create: create, diff --git a/src/web/registry.ts b/src/web/registry.ts index 7206d1d4..a290c3bd 100644 --- a/src/web/registry.ts +++ b/src/web/registry.ts @@ -96,9 +96,4 @@ export class UnistylesRegistry { applyStyles = (hash: string, value: UnistylesValues) => { this.css.add(hash, value) } - - reset = () => { - this.css.reset() - this.stylesCache.clear() - } } diff --git a/src/web/runtime.ts b/src/web/runtime.ts index 3ac3bbce..bc933439 100644 --- a/src/web/runtime.ts +++ b/src/web/runtime.ts @@ -205,12 +205,4 @@ export class UnistylesRuntime { return theme } - - getCSS = () => { - const css = this.services.registry.css.getStyles() - - this.services.registry.reset() - - return css - } } diff --git a/src/web/services.ts b/src/web/services.ts index bcb29a02..60ae699d 100644 --- a/src/web/services.ts +++ b/src/web/services.ts @@ -3,9 +3,8 @@ import { UnistylesRegistry } from './registry' import { UnistylesRuntime } from './runtime' import { UnistylesShadowRegistry } from './shadowRegistry' import { UnistylesState } from './state' -import { isServer } from './utils' -class UnistylesServices { +export class UnistylesServices { runtime: UnistylesRuntime registry: UnistylesRegistry shadowRegistry: UnistylesShadowRegistry @@ -27,13 +26,3 @@ class UnistylesServices { this.services.listener = this.listener } } - -declare global { - var __unistyles_web__: UnistylesServices -} - -if (isServer() && !globalThis.__unistyles__) { - globalThis.__unistyles_web__ = new UnistylesServices() -} - -export const UnistylesWeb = isServer() ? globalThis.__unistyles__ : new UnistylesServices() diff --git a/src/web/utils/unistyle.ts b/src/web/utils/unistyle.ts index 57ce3855..02008fdb 100644 --- a/src/web/utils/unistyle.ts +++ b/src/web/utils/unistyle.ts @@ -4,7 +4,7 @@ import type { StyleSheet, StyleSheetWithSuperPowers, UnistylesValues } from '../ import { isUnistylesMq, parseMq } from '../../mq' import { keyInObject, reduceObject } from './common' import type { UnistylesBreakpoints } from '../../global' -import { UnistylesWeb } from '../services' +import { UnistylesWeb } from '..' export const schemeToTheme = (scheme: ColorScheme) => { switch (scheme) { diff --git a/yarn.lock b/yarn.lock index 8400d9aa..02f0e5aa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3005,6 +3005,69 @@ __metadata: languageName: node linkType: hard +"@next/env@npm:15.1.1": + version: 15.1.1 + resolution: "@next/env@npm:15.1.1" + checksum: c73e11b161156d36e97b45a571f05088667db4484e5a1805eab93e8f2838b1b86d4aa07e0bf36a554bc2f9d98526efcbe9ed93337bca94c0a37c220778af7257 + languageName: node + linkType: hard + +"@next/swc-darwin-arm64@npm:15.1.1": + version: 15.1.1 + resolution: "@next/swc-darwin-arm64@npm:15.1.1" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@next/swc-darwin-x64@npm:15.1.1": + version: 15.1.1 + resolution: "@next/swc-darwin-x64@npm:15.1.1" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@next/swc-linux-arm64-gnu@npm:15.1.1": + version: 15.1.1 + resolution: "@next/swc-linux-arm64-gnu@npm:15.1.1" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@next/swc-linux-arm64-musl@npm:15.1.1": + version: 15.1.1 + resolution: "@next/swc-linux-arm64-musl@npm:15.1.1" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@next/swc-linux-x64-gnu@npm:15.1.1": + version: 15.1.1 + resolution: "@next/swc-linux-x64-gnu@npm:15.1.1" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@next/swc-linux-x64-musl@npm:15.1.1": + version: 15.1.1 + resolution: "@next/swc-linux-x64-musl@npm:15.1.1" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@next/swc-win32-arm64-msvc@npm:15.1.1": + version: 15.1.1 + resolution: "@next/swc-win32-arm64-msvc@npm:15.1.1" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@next/swc-win32-x64-msvc@npm:15.1.1": + version: 15.1.1 + resolution: "@next/swc-win32-x64-msvc@npm:15.1.1" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@nodelib/fs.scandir@npm:2.1.5": version: 2.1.5 resolution: "@nodelib/fs.scandir@npm:2.1.5" @@ -4049,6 +4112,22 @@ __metadata: languageName: node linkType: hard +"@swc/counter@npm:0.1.3": + version: 0.1.3 + resolution: "@swc/counter@npm:0.1.3" + checksum: df8f9cfba9904d3d60f511664c70d23bb323b3a0803ec9890f60133954173047ba9bdeabce28cd70ba89ccd3fd6c71c7b0bd58be85f611e1ffbe5d5c18616598 + languageName: node + linkType: hard + +"@swc/helpers@npm:0.5.15": + version: 0.5.15 + resolution: "@swc/helpers@npm:0.5.15" + dependencies: + tslib: ^2.8.0 + checksum: 1a9e0dbb792b2d1e0c914d69c201dbc96af3a0e6e6e8cf5a7f7d6a5d7b0e8b762915cd4447acb6b040e2ecc1ed49822875a7239f99a2d63c96c3c3407fb6fccf + languageName: node + linkType: hard + "@szmarczak/http-timer@npm:^5.0.1": version: 5.0.1 resolution: "@szmarczak/http-timer@npm:5.0.1" @@ -5270,6 +5349,15 @@ __metadata: languageName: node linkType: hard +"busboy@npm:1.6.0": + version: 1.6.0 + resolution: "busboy@npm:1.6.0" + dependencies: + streamsearch: ^1.1.0 + checksum: 32801e2c0164e12106bf236291a00795c3c4e4b709ae02132883fe8478ba2ae23743b11c5735a0aae8afe65ac4b6ca4568b91f0d9fed1fdbc32ede824a73746e + languageName: node + linkType: hard + "bytes@npm:3.1.2": version: 3.1.2 resolution: "bytes@npm:3.1.2" @@ -5379,6 +5467,13 @@ __metadata: languageName: node linkType: hard +"caniuse-lite@npm:^1.0.30001579": + version: 1.0.30001690 + resolution: "caniuse-lite@npm:1.0.30001690" + checksum: f2c1b595f15d8de4d9ccd155d61ac9f00ac62f1515870505a0186266fd52aef169fcddc90d8a4814e52b77107244806466fadc2c216662f23f1022a430e735ee + languageName: node + linkType: hard + "caniuse-lite@npm:^1.0.30001646, caniuse-lite@npm:^1.0.30001669": version: 1.0.30001687 resolution: "caniuse-lite@npm:1.0.30001687" @@ -5605,6 +5700,13 @@ __metadata: languageName: node linkType: hard +"client-only@npm:0.0.1": + version: 0.0.1 + resolution: "client-only@npm:0.0.1" + checksum: 0c16bf660dadb90610553c1d8946a7fdfb81d624adea073b8440b7d795d5b5b08beb3c950c6a2cf16279365a3265158a236876d92bce16423c485c322d7dfaf8 + languageName: node + linkType: hard + "cliui@npm:^6.0.0": version: 6.0.0 resolution: "cliui@npm:6.0.0" @@ -11583,7 +11685,7 @@ __metadata: languageName: node linkType: hard -"nanoid@npm:^3.3.7": +"nanoid@npm:^3.3.6, nanoid@npm:^3.3.7": version: 3.3.8 resolution: "nanoid@npm:3.3.8" bin: @@ -11650,6 +11752,67 @@ __metadata: languageName: node linkType: hard +"next@npm:15.1.1": + version: 15.1.1 + resolution: "next@npm:15.1.1" + dependencies: + "@next/env": 15.1.1 + "@next/swc-darwin-arm64": 15.1.1 + "@next/swc-darwin-x64": 15.1.1 + "@next/swc-linux-arm64-gnu": 15.1.1 + "@next/swc-linux-arm64-musl": 15.1.1 + "@next/swc-linux-x64-gnu": 15.1.1 + "@next/swc-linux-x64-musl": 15.1.1 + "@next/swc-win32-arm64-msvc": 15.1.1 + "@next/swc-win32-x64-msvc": 15.1.1 + "@swc/counter": 0.1.3 + "@swc/helpers": 0.5.15 + busboy: 1.6.0 + caniuse-lite: ^1.0.30001579 + postcss: 8.4.31 + sharp: ^0.33.5 + styled-jsx: 5.1.6 + peerDependencies: + "@opentelemetry/api": ^1.1.0 + "@playwright/test": ^1.41.2 + babel-plugin-react-compiler: "*" + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + dependenciesMeta: + "@next/swc-darwin-arm64": + optional: true + "@next/swc-darwin-x64": + optional: true + "@next/swc-linux-arm64-gnu": + optional: true + "@next/swc-linux-arm64-musl": + optional: true + "@next/swc-linux-x64-gnu": + optional: true + "@next/swc-linux-x64-musl": + optional: true + "@next/swc-win32-arm64-msvc": + optional: true + "@next/swc-win32-x64-msvc": + optional: true + sharp: + optional: true + peerDependenciesMeta: + "@opentelemetry/api": + optional: true + "@playwright/test": + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + bin: + next: dist/bin/next + checksum: 7ac5063c3fdb41501c21c2d5db6e6f52744376d28ddeb94980d21a0e5c93e2e33c95191cae158e50da1064bbc09ca730119413bfa66feda7f15a7330c0f5bfb0 + languageName: node + linkType: hard + "nitro-codegen@npm:0.18.2": version: 0.18.2 resolution: "nitro-codegen@npm:0.18.2" @@ -12565,6 +12728,17 @@ __metadata: languageName: node linkType: hard +"postcss@npm:8.4.31": + version: 8.4.31 + resolution: "postcss@npm:8.4.31" + dependencies: + nanoid: ^3.3.6 + picocolors: ^1.0.0 + source-map-js: ^1.0.2 + checksum: 1d8611341b073143ad90486fcdfeab49edd243377b1f51834dc4f6d028e82ce5190e4f11bb2633276864503654fb7cab28e67abdc0fbf9d1f88cad4a0ff0beea + languageName: node + linkType: hard + "postcss@npm:^7.0.2": version: 7.0.39 resolution: "postcss@npm:7.0.39" @@ -12956,6 +13130,7 @@ __metadata: 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 @@ -14080,7 +14255,7 @@ __metadata: languageName: node linkType: hard -"sharp@npm:0.33.5, sharp@npm:^0.33.3": +"sharp@npm:0.33.5, sharp@npm:^0.33.3, sharp@npm:^0.33.5": version: 0.33.5 resolution: "sharp@npm:0.33.5" dependencies: @@ -14296,7 +14471,7 @@ __metadata: languageName: node linkType: hard -"source-map-js@npm:^1.2.0, source-map-js@npm:^1.2.1": +"source-map-js@npm:^1.0.2, source-map-js@npm:^1.2.0, source-map-js@npm:^1.2.1": version: 1.2.1 resolution: "source-map-js@npm:1.2.1" checksum: 4eb0cd997cdf228bc253bcaff9340afeb706176e64868ecd20efbe6efea931465f43955612346d6b7318789e5265bdc419bc7669c1cebe3db0eb255f57efa76b @@ -14468,6 +14643,13 @@ __metadata: languageName: node linkType: hard +"streamsearch@npm:^1.1.0": + version: 1.1.0 + resolution: "streamsearch@npm:1.1.0" + checksum: 1cce16cea8405d7a233d32ca5e00a00169cc0e19fbc02aa839959985f267335d435c07f96e5e0edd0eadc6d39c98d5435fb5bbbdefc62c41834eadc5622ad942 + languageName: node + linkType: hard + "string-length@npm:^4.0.1": version: 4.0.2 resolution: "string-length@npm:4.0.2" @@ -14649,6 +14831,22 @@ __metadata: languageName: node linkType: hard +"styled-jsx@npm:5.1.6": + version: 5.1.6 + resolution: "styled-jsx@npm:5.1.6" + dependencies: + client-only: 0.0.1 + peerDependencies: + react: ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + peerDependenciesMeta: + "@babel/core": + optional: true + babel-plugin-macros: + optional: true + checksum: 879ad68e3e81adcf4373038aaafe55f968294955593660e173fbf679204aff158c59966716a60b29af72dc88795cfb2c479b6d2c3c87b2b2d282f3e27cc66461 + languageName: node + linkType: hard + "styleq@npm:^0.1.3": version: 0.1.3 resolution: "styleq@npm:0.1.3" @@ -14870,7 +15068,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.0.1, tslib@npm:^2.1.0, tslib@npm:^2.4.0": +"tslib@npm:^2.0.1, tslib@npm:^2.1.0, tslib@npm:^2.4.0, tslib@npm:^2.8.0": version: 2.8.1 resolution: "tslib@npm:2.8.1" checksum: e4aba30e632b8c8902b47587fd13345e2827fa639e7c3121074d5ee0880723282411a8838f830b55100cbe4517672f84a2472667d355b81e8af165a55dc6203a From cb84623f4af0c3e3df92180b12de1d7767662d7a Mon Sep 17 00:00:00 2001 From: Hubert Bieszczad Date: Thu, 19 Dec 2024 11:45:09 +0100 Subject: [PATCH 08/19] feat: static ssr --- src/web/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/web/index.ts b/src/web/index.ts index d275b103..76482fb1 100644 --- a/src/web/index.ts +++ b/src/web/index.ts @@ -7,14 +7,16 @@ 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_web__ : new UnistylesServices() +export const UnistylesWeb = isServer() ? globalThis.__unistyles__ : new UnistylesServices() export const StyleSheet = { configure: UnistylesWeb.state.init, create: create, From 0f882205855e8879966e807a0977b3fadd20ed55 Mon Sep 17 00:00:00 2001 From: Hubert Bieszczad Date: Thu, 19 Dec 2024 12:03:08 +0100 Subject: [PATCH 09/19] feat: static ssr --- src/server/useServerUnistyles.tsx | 2 +- src/web/registry.ts | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/server/useServerUnistyles.tsx b/src/server/useServerUnistyles.tsx index 7b4d5b53..a171881f 100644 --- a/src/server/useServerUnistyles.tsx +++ b/src/server/useServerUnistyles.tsx @@ -17,7 +17,7 @@ export const useServerUnistyles = () => { const css = UnistylesWeb.registry.css.getStyles() const state = UnistylesWeb.registry.css.getState() - UnistylesWeb.registry.css.reset() + UnistylesWeb.registry.reset() return ( <> diff --git a/src/web/registry.ts b/src/web/registry.ts index a290c3bd..dcced89e 100644 --- a/src/web/registry.ts +++ b/src/web/registry.ts @@ -96,4 +96,12 @@ export class UnistylesRegistry { applyStyles = (hash: string, value: UnistylesValues) => { this.css.add(hash, value) } + + reset = () => { + this.css.reset() + this.stylesCache.clear() + this.dependenciesMap.clear() + this.disposeListenersMap.clear() + this.stylesCounter.clear() + } } From 6e3e3624bd6e4a7bcea1053e4cc3f635a4af4f15 Mon Sep 17 00:00:00 2001 From: Hubert Bieszczad Date: Thu, 19 Dec 2024 12:09:02 +0100 Subject: [PATCH 10/19] feat: inject rnw styles --- src/server/useServerUnistyles.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/server/useServerUnistyles.tsx b/src/server/useServerUnistyles.tsx index a171881f..653b0c8c 100644 --- a/src/server/useServerUnistyles.tsx +++ b/src/server/useServerUnistyles.tsx @@ -1,4 +1,5 @@ import React, { useRef } from 'react' +import { StyleSheet } from 'react-native' import { useServerInsertedHTML } from 'next/navigation' import { UnistylesWeb } from '../web' @@ -15,12 +16,15 @@ export const useServerUnistyles = () => { 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 ( <> + From f7ef8b13c2fe6f9b1f241fb396895f9c7051ec3f Mon Sep 17 00:00:00 2001 From: Hubert Bieszczad Date: Thu, 19 Dec 2024 13:02:25 +0100 Subject: [PATCH 11/19] feat: hydrate mq state --- src/web/css/state.ts | 60 +++++++++++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/src/web/css/state.ts b/src/web/css/state.ts index 076bcfa7..14fb0f87 100644 --- a/src/web/css/state.ts +++ b/src/web/css/state.ts @@ -11,6 +11,7 @@ type SetthisProps = { propertyKey: string value: any } +type HydrateState = Array<[ string, Array<[ string, Array<[ string, any ]> ]> ]> const safeGetMap = (map: Map>, key: string) => { const nextLevelMap = map.get(key) @@ -113,34 +114,47 @@ export class CSSState { } getState = () => { - return Array.from(this.mainMap.entries()).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 Array<[ string, Array<[ string, Array<[ string, any ]> ]> ]> + 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 = (state: ReturnType) => { - state.forEach(([mediaQuery, classNames]) => { - classNames.forEach(([className, style]) => { - style.forEach(([propertyKey, value]) => { - this.set({ - className, - propertyKey, - value, - mediaQuery + 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 = () => { From 7a92b55e53ac93bb8b4a7a0b62a951211d61abec Mon Sep 17 00:00:00 2001 From: Hubert Bieszczad Date: Fri, 20 Dec 2024 07:39:42 +0100 Subject: [PATCH 12/19] fix: append style tag to head --- src/web/css/state.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/web/css/state.ts b/src/web/css/state.ts index 14fb0f87..e737c6af 100644 --- a/src/web/css/state.ts +++ b/src/web/css/state.ts @@ -47,6 +47,7 @@ export class CSSState { this.styleTag = document.createElement('style') this.styleTag.id = 'unistyles-web' + document.head.appendChild(this.styleTag) } set = ({ className, propertyKey, value, mediaQuery = '', isMq }: SetthisProps) => { From 144ac57ed8e76c4263223f91c95e33ef67808092 Mon Sep 17 00:00:00 2001 From: Hubert Bieszczad Date: Fri, 20 Dec 2024 07:53:48 +0100 Subject: [PATCH 13/19] chore: remove getClassName export --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 26ba3dfe..181e283d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ export { StyleSheet, UnistylesRuntime, StatusBar, NavigationBar } from './specs' export { mq } from './mq' export type { UnistylesThemes, UnistylesBreakpoints } from './global' -export { createUnistylesComponent, withUnistyles, getClassName } from './core' +export { createUnistylesComponent, withUnistyles } from './core' export type { UnistylesVariants } from './types' export { Display, Hide, Variants, ScopedTheme } from './components' From 2a9ffc7fa9f59f5bd72dbf92d48942b3dacb7c4b Mon Sep 17 00:00:00 2001 From: Hubert Bieszczad Date: Fri, 20 Dec 2024 08:03:05 +0100 Subject: [PATCH 14/19] feat: static ImageBackground --- .../createUnistylesImageBackground.native.tsx | 24 ++++++ src/core/createUnistylesImageBackground.tsx | 79 +++++++++++++------ 2 files changed, 81 insertions(+), 22 deletions(-) create mode 100644 src/core/createUnistylesImageBackground.native.tsx diff --git a/src/core/createUnistylesImageBackground.native.tsx b/src/core/createUnistylesImageBackground.native.tsx new file mode 100644 index 00000000..1476688d --- /dev/null +++ b/src/core/createUnistylesImageBackground.native.tsx @@ -0,0 +1,24 @@ +import React from 'react' +import type { ImageBackground, ImageBackgroundProps } from 'react-native' +import { UnistylesShadowRegistry } from '../specs' +import { passForwardedRef } from './passForwardRef' + +export const createUnistylesImageBackground = (Component: typeof ImageBackground) => React.forwardRef((props, forwardedRef) => ( + passForwardedRef(props, ref, forwardedRef)} + imageRef={ref => { + const style = Array.isArray(props.imageStyle) + ? props.imageStyle + : [props.imageStyle] + + // @ts-expect-error web types are not compatible with RN styles + UnistylesShadowRegistry.add(ref, style) + + return () => { + // @ts-expect-error hidden from TS + UnistylesShadowRegistry.remove(ref) + } + }} + /> +)) diff --git a/src/core/createUnistylesImageBackground.tsx b/src/core/createUnistylesImageBackground.tsx index 1476688d..3bb33038 100644 --- a/src/core/createUnistylesImageBackground.tsx +++ b/src/core/createUnistylesImageBackground.tsx @@ -1,24 +1,59 @@ import React from 'react' -import type { ImageBackground, ImageBackgroundProps } from 'react-native' -import { UnistylesShadowRegistry } from '../specs' -import { passForwardedRef } from './passForwardRef' - -export const createUnistylesImageBackground = (Component: typeof ImageBackground) => React.forwardRef((props, forwardedRef) => ( - passForwardedRef(props, ref, forwardedRef)} - imageRef={ref => { - const style = Array.isArray(props.imageStyle) - ? props.imageStyle - : [props.imageStyle] - - // @ts-expect-error web types are not compatible with RN styles - UnistylesShadowRegistry.add(ref, style) - - return () => { +import { getClassName } from './getClassname' +import type { UnistylesValues } from '../types' +import { isServer } from '../web/utils' + +type ComponentProps = { + style?: UnistylesValues | Array +} + +export const createUnistylesImageBackground = (Component: any) => React.forwardRef((props, forwardedRef) => { + let storedRef: HTMLElement | null = null + let storedImageRef: HTMLElement | null = null + const classNames = getClassName(props.style) + const imageClassNames = getClassName(props.style) + + return ( + { + 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) + + if (typeof forwardedRef === 'function') { + return forwardedRef(ref) + } + + if (forwardedRef) { + forwardedRef.current = ref + } + }} + ref={isServer() ? undefined : (ref: HTMLElement | null) => { + if (!ref) { + // @ts-expect-error hidden from TS + UnistylesShadowRegistry.remove(storedRef, classNames?.hash) + } + + storedRef = ref // @ts-expect-error hidden from TS - UnistylesShadowRegistry.remove(ref) - } - }} - /> -)) + UnistylesShadowRegistry.add(ref, classNames?.hash) + + if (typeof forwardedRef === 'function') { + return forwardedRef(ref) + } + + if (forwardedRef) { + forwardedRef.current = ref + } + }} + /> + ) +}) From 25f2583b43c6bfad31f656206a114c524cb77ade Mon Sep 17 00:00:00 2001 From: Hubert Bieszczad Date: Fri, 20 Dec 2024 08:03:42 +0100 Subject: [PATCH 15/19] feat: dispose listeners when removing class --- src/web/registry.ts | 4 ++++ src/web/shadowRegistry.ts | 7 ++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/web/registry.ts b/src/web/registry.ts index dcced89e..ca4f4a5e 100644 --- a/src/web/registry.ts +++ b/src/web/registry.ts @@ -77,7 +77,11 @@ export class UnistylesRegistry { if (stylesCounter.size === 0 && !document.querySelector(hash)) { this.css.remove(hash) this.stylesCache.delete(hash) + + return true } + + return false } add = (value: UnistylesValues) => { diff --git a/src/web/shadowRegistry.ts b/src/web/shadowRegistry.ts index 8d6e3b53..057d44bf 100644 --- a/src/web/shadowRegistry.ts +++ b/src/web/shadowRegistry.ts @@ -97,6 +97,11 @@ export class UnistylesShadowRegistry { return } - this.services.registry.remove(ref, hash) + const removed = this.services.registry.remove(ref, hash) + + if (removed) { + this.disposeMap.get(hash)?.() + this.disposeMap.delete(hash) + } } } From 415cb5d3ef625e9576e62c25de810ac2be19f4dc Mon Sep 17 00:00:00 2001 From: Hubert Bieszczad Date: Fri, 20 Dec 2024 08:38:40 +0100 Subject: [PATCH 16/19] fix: static --- src/utils.ts | 26 +++++++++++++++----------- src/web/registry.ts | 2 +- src/web/variants.ts | 4 ++-- 3 files changed, 18 insertions(+), 14 deletions(-) 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/registry.ts b/src/web/registry.ts index ca4f4a5e..38a36c94 100644 --- a/src/web/registry.ts +++ b/src/web/registry.ts @@ -74,7 +74,7 @@ export class UnistylesRegistry { stylesCounter.delete(ref) - if (stylesCounter.size === 0 && !document.querySelector(hash)) { + if (stylesCounter.size === 0 && !document.querySelector(`.${hash}`)) { this.css.remove(hash) this.stylesCache.delete(hash) diff --git a/src/web/variants.ts b/src/web/variants.ts index a4ba0c38..eb236f1b 100644 --- a/src/web/variants.ts +++ b/src/web/variants.ts @@ -1,5 +1,5 @@ import type { ReactNativeStyleSheet, StyleSheet } from '../types' -import { deepMergeObjects } from '../utils' +import { deepMergeObjects, isDefined } from '../utils' type StylesWithVariants = { variants: Record, @@ -7,7 +7,7 @@ type StylesWithVariants = { styles: Record }> } -const hasVariants = (value: [string, T]): value is [string, T & StylesWithVariants] => 'variants' in value[1] +const hasVariants = (value: [string, T]): value is [string, T & StylesWithVariants] => isDefined(value[1]) && 'variants' in value[1] export const getVariants = (styles: ReactNativeStyleSheet, selectedVariants: Record) => { return Object.entries(styles) From 0b3607917851a81dff834d3b1068402f83222013 Mon Sep 17 00:00:00 2001 From: Hubert Bieszczad Date: Fri, 20 Dec 2024 12:05:15 +0100 Subject: [PATCH 17/19] feat: imageBackground web --- src/components/native/ImageBackground.tsx | 19 +++-- .../createUnistylesImageBackground.native.tsx | 24 ------ src/core/createUnistylesImageBackground.tsx | 79 ++++++------------- 3 files changed, 35 insertions(+), 87 deletions(-) delete mode 100644 src/core/createUnistylesImageBackground.native.tsx diff --git a/src/components/native/ImageBackground.tsx b/src/components/native/ImageBackground.tsx index e0a7681a..45228d3c 100644 --- a/src/components/native/ImageBackground.tsx +++ b/src/components/native/ImageBackground.tsx @@ -13,6 +13,7 @@ type Props = { 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) @@ -26,15 +27,11 @@ export const ImageBackground = forwardRef((props, forwardedRef) if (!ref) { // @ts-expect-error hidden from TS UnistylesShadowRegistry.remove(storedRef, styleClassNames?.hash) - // @ts-expect-error hidden from TS - UnistylesShadowRegistry.remove(storedRef, imageClassNames?.hash) } storedRef = ref - // @ts-expect-error hidden from TS - UnistylesShadowRegistry.add(ref, styleClassNames?.hash) - // @ts-expect-error hidden from TS - UnistylesShadowRegistry.add(ref, imageClassNames?.hash) + // @ts-expect-error hidden from TS + UnistylesShadowRegistry.add(ref, styleClassNames?.hash) if (typeof forwardedRef === 'function') { return forwardedRef(ref) @@ -44,6 +41,16 @@ export const ImageBackground = forwardRef((props, 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/core/createUnistylesImageBackground.native.tsx b/src/core/createUnistylesImageBackground.native.tsx deleted file mode 100644 index 1476688d..00000000 --- a/src/core/createUnistylesImageBackground.native.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react' -import type { ImageBackground, ImageBackgroundProps } from 'react-native' -import { UnistylesShadowRegistry } from '../specs' -import { passForwardedRef } from './passForwardRef' - -export const createUnistylesImageBackground = (Component: typeof ImageBackground) => React.forwardRef((props, forwardedRef) => ( - passForwardedRef(props, ref, forwardedRef)} - imageRef={ref => { - const style = Array.isArray(props.imageStyle) - ? props.imageStyle - : [props.imageStyle] - - // @ts-expect-error web types are not compatible with RN styles - UnistylesShadowRegistry.add(ref, style) - - return () => { - // @ts-expect-error hidden from TS - UnistylesShadowRegistry.remove(ref) - } - }} - /> -)) diff --git a/src/core/createUnistylesImageBackground.tsx b/src/core/createUnistylesImageBackground.tsx index 3bb33038..1476688d 100644 --- a/src/core/createUnistylesImageBackground.tsx +++ b/src/core/createUnistylesImageBackground.tsx @@ -1,59 +1,24 @@ import React from 'react' -import { getClassName } from './getClassname' -import type { UnistylesValues } from '../types' -import { isServer } from '../web/utils' - -type ComponentProps = { - style?: UnistylesValues | Array -} - -export const createUnistylesImageBackground = (Component: any) => React.forwardRef((props, forwardedRef) => { - let storedRef: HTMLElement | null = null - let storedImageRef: HTMLElement | null = null - const classNames = getClassName(props.style) - const imageClassNames = getClassName(props.style) - - return ( - { - 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) - - if (typeof forwardedRef === 'function') { - return forwardedRef(ref) - } - - if (forwardedRef) { - forwardedRef.current = ref - } - }} - ref={isServer() ? undefined : (ref: HTMLElement | null) => { - if (!ref) { - // @ts-expect-error hidden from TS - UnistylesShadowRegistry.remove(storedRef, classNames?.hash) - } - - storedRef = ref +import type { ImageBackground, ImageBackgroundProps } from 'react-native' +import { UnistylesShadowRegistry } from '../specs' +import { passForwardedRef } from './passForwardRef' + +export const createUnistylesImageBackground = (Component: typeof ImageBackground) => React.forwardRef((props, forwardedRef) => ( + passForwardedRef(props, ref, forwardedRef)} + imageRef={ref => { + const style = Array.isArray(props.imageStyle) + ? props.imageStyle + : [props.imageStyle] + + // @ts-expect-error web types are not compatible with RN styles + UnistylesShadowRegistry.add(ref, style) + + return () => { // @ts-expect-error hidden from TS - UnistylesShadowRegistry.add(ref, classNames?.hash) - - if (typeof forwardedRef === 'function') { - return forwardedRef(ref) - } - - if (forwardedRef) { - forwardedRef.current = ref - } - }} - /> - ) -}) + UnistylesShadowRegistry.remove(ref) + } + }} + /> +)) From b57ee8888003ca146e4f79d9f8d90285aac21ce2 Mon Sep 17 00:00:00 2001 From: Hubert Bieszczad Date: Fri, 20 Dec 2024 12:26:54 +0100 Subject: [PATCH 18/19] chore: change typo in type --- src/web/css/state.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/web/css/state.ts b/src/web/css/state.ts index e737c6af..08ac7759 100644 --- a/src/web/css/state.ts +++ b/src/web/css/state.ts @@ -4,7 +4,7 @@ import { hyphenate, isServer } from '../utils' import { convertToCSS } from './core' type MapType = Map>> -type SetthisProps = { +type SetProps = { mediaQuery?: string className: string isMq?: boolean @@ -50,7 +50,7 @@ export class CSSState { document.head.appendChild(this.styleTag) } - set = ({ className, propertyKey, value, mediaQuery = '', isMq }: SetthisProps) => { + set = ({ className, propertyKey, value, mediaQuery = '', isMq }: SetProps) => { const firstLevelMap = isMq ? this.mqMap : this.mainMap const secondLevelMap = safeGetMap(firstLevelMap, mediaQuery) const thirdLevelMap = safeGetMap(secondLevelMap, className) From 63e74101d513a1953a1684aa7743d9bcb97767ac Mon Sep 17 00:00:00 2001 From: Hubert Bieszczad Date: Fri, 20 Dec 2024 13:00:30 +0100 Subject: [PATCH 19/19] chore: cr --- src/components/ScopedTheme.tsx | 2 +- src/server/useServerUnistyles.tsx | 6 +++--- src/web/convert/object/filter.ts | 2 +- src/web/create.ts | 4 ++-- src/web/utils/unistyle.ts | 4 ++-- src/web/variants.ts | 6 +++++- 6 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/components/ScopedTheme.tsx b/src/components/ScopedTheme.tsx index dc9452a9..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/server/useServerUnistyles.tsx b/src/server/useServerUnistyles.tsx index 653b0c8c..7d33d368 100644 --- a/src/server/useServerUnistyles.tsx +++ b/src/server/useServerUnistyles.tsx @@ -5,7 +5,7 @@ import { UnistylesWeb } from '../web' declare global { interface Window { - UNISTYLES_STATE: any + __UNISTYLES_STATE__: ReturnType } } @@ -26,7 +26,7 @@ export const useServerUnistyles = () => { <> - + ) } @@ -35,6 +35,6 @@ export const useServerUnistyles = () => { }) if (typeof window !== 'undefined') { - UnistylesWeb.registry.css.hydrate(window.UNISTYLES_STATE) + UnistylesWeb.registry.css.hydrate(window.__UNISTYLES_STATE__) } } diff --git a/src/web/convert/object/filter.ts b/src/web/convert/object/filter.ts index f7eca354..89046a97 100644 --- a/src/web/convert/object/filter.ts +++ b/src/web/convert/object/filter.ts @@ -4,7 +4,7 @@ import type { Filters } from '../types' import { getObjectStyle } from './objectStyle' import { normalizeColor, normalizeNumericValue } from '../utils' import { isUnistylesMq } from '../../../mq' -import { UnistylesWeb } from '../..' +import { UnistylesWeb } from '../../index' const getDropShadowStyle = (dropShadow: DropShadowValue) => { const { offsetX = 0, offsetY = 0, standardDeviation = 0, color = '#000' } = dropShadow diff --git a/src/web/create.ts b/src/web/create.ts index d5c0d199..46b56fc0 100644 --- a/src/web/create.ts +++ b/src/web/create.ts @@ -1,5 +1,5 @@ import type { StyleSheetWithSuperPowers, StyleSheet } from '../types/stylesheet' -import { UnistylesWeb } from '.' +import { UnistylesWeb } from './index' import { assignSecrets, error, removeInlineStyles } from './utils' const useVariants = ['useVariants', () => {}] @@ -10,7 +10,7 @@ export const create = (stylesheet: StyleSheetWithSuperPowers, id?: s } const computedStylesheet = typeof stylesheet === 'function' - ? stylesheet(UnistylesWeb.runtime.theme, UnistylesWeb.runtime.miniRuntime) + ? stylesheet(UnistylesWeb.runtime.theme, UnistylesWeb.runtime.miniRuntime) : stylesheet const addSecrets = (value: any, key: string, args?: Array) => assignSecrets(value, { diff --git a/src/web/utils/unistyle.ts b/src/web/utils/unistyle.ts index 02008fdb..8c3c2e94 100644 --- a/src/web/utils/unistyle.ts +++ b/src/web/utils/unistyle.ts @@ -4,7 +4,7 @@ import type { StyleSheet, StyleSheetWithSuperPowers, UnistylesValues } from '../ import { isUnistylesMq, parseMq } from '../../mq' import { keyInObject, reduceObject } from './common' import type { UnistylesBreakpoints } from '../../global' -import { UnistylesWeb } from '..' +import { UnistylesWeb } from '../index' export const schemeToTheme = (scheme: ColorScheme) => { switch (scheme) { @@ -66,7 +66,7 @@ export const getMediaQuery = (query: string, allBreakpoints: Array) => { const breakpointValue = UnistylesWeb.runtime.breakpoints[query as keyof UnistylesBreakpoints] ?? 0 const nextBreakpoint = allBreakpoints .filter((b): b is keyof UnistylesBreakpoints => b in UnistylesWeb.runtime.breakpoints) - .map(b => UnistylesWeb.runtime.breakpoints[b] ?? 1) + .map(b => UnistylesWeb.runtime.breakpoints[b] as number) .sort((a, b) => a - b) .find(b => b > breakpointValue) const queries = [ diff --git a/src/web/variants.ts b/src/web/variants.ts index eb236f1b..4359443c 100644 --- a/src/web/variants.ts +++ b/src/web/variants.ts @@ -7,7 +7,11 @@ type StylesWithVariants = { styles: Record }> } -const hasVariants = (value: [string, T]): value is [string, T & StylesWithVariants] => isDefined(value[1]) && 'variants' in value[1] +const hasVariants = (value: [string, T]): value is [string, T & StylesWithVariants] => { + const [, styleValue] = value + + return isDefined(styleValue) && 'variants' in styleValue +} export const getVariants = (styles: ReactNativeStyleSheet, selectedVariants: Record) => { return Object.entries(styles)