From 1ebbcdbbbb766afd3be81ab86f90ad511784042a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81opaci=C5=84ski?= Date: Wed, 3 Dec 2025 03:21:08 +0100 Subject: [PATCH 1/7] Start working on worklet-compatible style builders --- .../__tests__/createStyleBuilder.test.ts | 2 +- .../common/style/createStyleBuilder-old.ts | 103 +++++++++ .../src/common/style/createStyleBuilder.ts | 195 +++++++++--------- .../src/common/style/index.ts | 3 +- .../src/common/style/propsBuilder.ts | 38 ++++ .../src/common/types/config.ts | 4 +- .../src/common/types/helpers.ts | 2 + .../src/updateProps/updateProps.ts | 33 +-- 8 files changed, 254 insertions(+), 126 deletions(-) create mode 100644 packages/react-native-reanimated/src/common/style/createStyleBuilder-old.ts create mode 100644 packages/react-native-reanimated/src/common/style/propsBuilder.ts diff --git a/packages/react-native-reanimated/src/common/style/__tests__/createStyleBuilder.test.ts b/packages/react-native-reanimated/src/common/style/__tests__/createStyleBuilder.test.ts index 49acbfb72474..fb98c3376088 100644 --- a/packages/react-native-reanimated/src/common/style/__tests__/createStyleBuilder.test.ts +++ b/packages/react-native-reanimated/src/common/style/__tests__/createStyleBuilder.test.ts @@ -1,7 +1,7 @@ 'use strict'; import type { PlainStyle } from '../../types'; import { ValueProcessorTarget } from '../../types'; -import createStyleBuilder from '../createStyleBuilder'; +import createStyleBuilder from '../createStyleBuilder-old'; // TODO - add more tests describe(createStyleBuilder, () => { diff --git a/packages/react-native-reanimated/src/common/style/createStyleBuilder-old.ts b/packages/react-native-reanimated/src/common/style/createStyleBuilder-old.ts new file mode 100644 index 000000000000..991bcd2180b8 --- /dev/null +++ b/packages/react-native-reanimated/src/common/style/createStyleBuilder-old.ts @@ -0,0 +1,103 @@ +'use strict'; + +import type { AnyRecord, ValueProcessorContext } from '../types'; +import { ValueProcessorTarget } from '../types'; +import { isConfigPropertyAlias, isDefined, isRecord } from '../utils'; +import type { + StyleBuilder, + StyleBuilderConfig, + StyleBuildMiddleware, +} from './types'; + +type StyleBuilderOptions

= { + buildMiddleware?: StyleBuildMiddleware

; + separatelyInterpolatedNestedProperties?: (keyof P)[]; + target?: ValueProcessorTarget; +}; + +class StyleBuilderImpl

implements StyleBuilder

{ + private readonly buildMiddleware: StyleBuildMiddleware

; + private readonly config: StyleBuilderConfig

; + private readonly separatelyInterpolatedNestedProperties_: (keyof P)[]; + private readonly context: ValueProcessorContext; + + private processedProps = {} as P; + + constructor(config: StyleBuilderConfig

, options?: StyleBuilderOptions

) { + this.config = config; + this.buildMiddleware = options?.buildMiddleware ?? ((props) => props); + this.separatelyInterpolatedNestedProperties_ = + options?.separatelyInterpolatedNestedProperties ?? []; + this.context = { + target: options?.target ?? ValueProcessorTarget.Default, + }; + } + + isSeparatelyInterpolatedNestedProperty(property: keyof P): boolean { + return this.separatelyInterpolatedNestedProperties_.includes(property); + } + + add(property: keyof P, value: P[keyof P]): void { + const configValue = this.config[property]; + + if (!configValue || !isDefined(value)) { + return; + } + + if (configValue === true) { + this.maybeAssignProp(property, value); + } else if (isConfigPropertyAlias

(configValue)) { + this.add(configValue.as, value); + } else { + const { process } = configValue; + const processedValue = process ? process(value, this.context) : value; + + if (!isDefined(processedValue)) { + return; + } + + if (isRecord

(processedValue)) { + this.maybeAssignProps(processedValue); + } else { + this.maybeAssignProp(property, processedValue); + } + } + } + + build(): P | null { + const result = this.buildMiddleware(this.processedProps); + this.cleanup(); + + if (Object.keys(result).length === 0) { + return null; + } + + return result; + } + + buildFrom(props: P): P | null { + Object.entries(props).forEach(([key, value]) => this.add(key, value)); + return this.build(); + } + + private maybeAssignProp(property: keyof P, value: P[keyof P]) { + this.processedProps[property] ??= value; + } + + private maybeAssignProps(props: P) { + Object.entries(props).forEach(([key, value]) => + this.maybeAssignProp(key, value) + ); + } + + private cleanup() { + this.processedProps = {} as P; + } +} + +export default function createStyleBuilder

( + config: StyleBuilderConfig

, + options?: StyleBuilderOptions

+): StyleBuilder> { + return new StyleBuilderImpl(config, options); +} diff --git a/packages/react-native-reanimated/src/common/style/createStyleBuilder.ts b/packages/react-native-reanimated/src/common/style/createStyleBuilder.ts index 991bcd2180b8..8f42dbfada4e 100644 --- a/packages/react-native-reanimated/src/common/style/createStyleBuilder.ts +++ b/packages/react-native-reanimated/src/common/style/createStyleBuilder.ts @@ -1,103 +1,108 @@ 'use strict'; - -import type { AnyRecord, ValueProcessorContext } from '../types'; -import { ValueProcessorTarget } from '../types'; -import { isConfigPropertyAlias, isDefined, isRecord } from '../utils'; -import type { - StyleBuilder, - StyleBuilderConfig, - StyleBuildMiddleware, -} from './types'; - -type StyleBuilderOptions

= { - buildMiddleware?: StyleBuildMiddleware

; - separatelyInterpolatedNestedProperties?: (keyof P)[]; - target?: ValueProcessorTarget; +import { ReanimatedError } from '../errors'; +import type { ReadonlyRecord, UnknownRecord, ValueProcessor } from '../types'; + +const MAX_PROCESS_DEPTH = 10; + +type PropsBuilderConfig = { + config: ReadonlyRecord; + processConfigEntry: (params: { + configValue: TConfigValue; + config: ReadonlyRecord; + // ctx: TConfigContext; + }) => ValueProcessor | TConfigValue | undefined; + buildProps: (props: Readonly) => TBuildResult; }; -class StyleBuilderImpl

implements StyleBuilder

{ - private readonly buildMiddleware: StyleBuildMiddleware

; - private readonly config: StyleBuilderConfig

; - private readonly separatelyInterpolatedNestedProperties_: (keyof P)[]; - private readonly context: ValueProcessorContext; - - private processedProps = {} as P; - - constructor(config: StyleBuilderConfig

, options?: StyleBuilderOptions

) { - this.config = config; - this.buildMiddleware = options?.buildMiddleware ?? ((props) => props); - this.separatelyInterpolatedNestedProperties_ = - options?.separatelyInterpolatedNestedProperties ?? []; - this.context = { - target: options?.target ?? ValueProcessorTarget.Default, - }; - } - - isSeparatelyInterpolatedNestedProperty(property: keyof P): boolean { - return this.separatelyInterpolatedNestedProperties_.includes(property); - } - - add(property: keyof P, value: P[keyof P]): void { - const configValue = this.config[property]; - - if (!configValue || !isDefined(value)) { - return; - } - - if (configValue === true) { - this.maybeAssignProp(property, value); - } else if (isConfigPropertyAlias

(configValue)) { - this.add(configValue.as, value); - } else { - const { process } = configValue; - const processedValue = process ? process(value, this.context) : value; - - if (!isDefined(processedValue)) { - return; +//& { ctx: TConfigContext }; + +export default function createPropsBuilder({ + config, + processConfigEntry, + buildProps, +}: PropsBuilderConfig) { + const processedConfig = Object.entries(config).reduce< + Record unknown> + >((acc, [key, configValue]) => { + let processedEntry: ReturnType = configValue; + + let depth = 0; + do { + if (++depth > MAX_PROCESS_DEPTH) { + throw new ReanimatedError( + `Max process depth for style builder reached for property ${key}` + ); } - - if (isRecord

(processedValue)) { - this.maybeAssignProps(processedValue); - } else { - this.maybeAssignProp(property, processedValue); - } - } - } - - build(): P | null { - const result = this.buildMiddleware(this.processedProps); - this.cleanup(); - - if (Object.keys(result).length === 0) { - return null; + processedEntry = processConfigEntry({ + configValue: processedEntry, + config, + }); + } while (processedEntry && typeof processedEntry !== 'function'); + + if (processedEntry) { + acc[key] = processedEntry as ValueProcessor; } - return result; - } - - buildFrom(props: P): P | null { - Object.entries(props).forEach(([key, value]) => this.add(key, value)); - return this.build(); - } - - private maybeAssignProp(property: keyof P, value: P[keyof P]) { - this.processedProps[property] ??= value; - } - - private maybeAssignProps(props: P) { - Object.entries(props).forEach(([key, value]) => - this.maybeAssignProp(key, value) - ); - } - - private cleanup() { - this.processedProps = {} as P; - } + return acc; + }, {}); + + return { + build(props: Readonly, includeUndefined = false) { + 'worklet'; + return buildProps( + Object.entries(props).reduce((acc, [key, value]) => { + const processedValue = processedConfig[key](value); // TODO - add value processor context + if (includeUndefined || processedValue !== undefined) { + acc[key] = processedValue; + } + return acc; + }, {}) + ); + }, + }; } -export default function createStyleBuilder

( - config: StyleBuilderConfig

, - options?: StyleBuilderOptions

-): StyleBuilder> { - return new StyleBuilderImpl(config, options); -} +// // <<<<<<< WEB STYLE BUILDER >>>>>>> + +// const isRuleBuilder =

( +// value: unknown +// ): value is RuleBuilder

=> value instanceof RuleBuilderImpl; + +// const webStyleBuilder = createStyleBuilder({ +// config: WEB_BASE_PROPERTIES_CONFIG, +// ctx: { +// ruleBuildersSet: new Set>(), +// }, +// process: ({ configValue, config, ctx }) => { +// if (configValue === true) { +// return (value) => String(value); +// } +// if (typeof configValue === 'string') { +// return (value) => +// hasSuffix(value) ? value : `${String(value)}${configValue}`; +// } +// if (isConfigPropertyAlias(configValue)) { +// return config[configValue.as]; +// } +// if (hasValueProcessor(configValue)) { +// return (value) => configValue.process(value); +// } +// if (isRuleBuilder(configValue)) { +// return (value) => { +// ctx.ruleBuildersSet.add(configValue); +// configValue.add(value); +// }; +// } +// }, +// build: (props) => { +// const entries = Object.entries(props); + +// if (entries.length === 0) { +// return null; +// } + +// return entries +// .map(([key, value]) => `${kebabizeCamelCase(key)}: ${String(value)}`) +// .join('; '); +// }, +// }); diff --git a/packages/react-native-reanimated/src/common/style/index.ts b/packages/react-native-reanimated/src/common/style/index.ts index f74aa304acf5..70db2e652fde 100644 --- a/packages/react-native-reanimated/src/common/style/index.ts +++ b/packages/react-native-reanimated/src/common/style/index.ts @@ -1,6 +1,7 @@ 'use strict'; export * from './config'; -export { default as createStyleBuilder } from './createStyleBuilder'; +export { default as createStyleBuilder } from './createStyleBuilder-old'; export * from './processors'; +export { default as styleBuilder } from './propsBuilder'; export type { StyleBuilder } from './types'; export type * from './types'; diff --git a/packages/react-native-reanimated/src/common/style/propsBuilder.ts b/packages/react-native-reanimated/src/common/style/propsBuilder.ts new file mode 100644 index 000000000000..378a72de43a2 --- /dev/null +++ b/packages/react-native-reanimated/src/common/style/propsBuilder.ts @@ -0,0 +1,38 @@ +'use strict'; +import type { ValueProcessor } from '../types'; +import { isConfigPropertyAlias, isRecord } from '../utils'; +import { BASE_PROPERTIES_CONFIG } from './config'; +import createStyleBuilder from './createStyleBuilder'; + +const hasValueProcessor = ( + configValue: unknown +): configValue is { process: ValueProcessor } => + isRecord(configValue) && 'process' in configValue; + +// TODO - maybe rename to propsBuilder, as we have updateProps +const propsBuilder = createStyleBuilder({ + config: BASE_PROPERTIES_CONFIG, + processConfigEntry: ({ configValue, config }) => { + if (configValue === true) { + return (value) => { + 'worklet'; + return value; + }; + } + if (isConfigPropertyAlias(configValue)) { + return config[configValue.as]; + } + if (hasValueProcessor(configValue)) { + return (value) => { + 'worklet'; + return configValue.process(value); + }; + } + }, + buildProps: (props) => { + 'worklet'; + return props; + }, +}); + +export default propsBuilder; diff --git a/packages/react-native-reanimated/src/common/types/config.ts b/packages/react-native-reanimated/src/common/types/config.ts index 0de0902fe665..0d1bff1e6d0e 100644 --- a/packages/react-native-reanimated/src/common/types/config.ts +++ b/packages/react-native-reanimated/src/common/types/config.ts @@ -11,10 +11,10 @@ export type ValueProcessorContext = { target: ValueProcessorTarget; }; -export type ValueProcessor = ( +export type ValueProcessor = ( value: NonMutable, context?: ValueProcessorContext -) => Maybe | Record; +) => R | Record; export type ConfigPropertyAlias

= { as: keyof P; diff --git a/packages/react-native-reanimated/src/common/types/helpers.ts b/packages/react-native-reanimated/src/common/types/helpers.ts index 6a924c7633d0..3e8e30d989ce 100644 --- a/packages/react-native-reanimated/src/common/types/helpers.ts +++ b/packages/react-native-reanimated/src/common/types/helpers.ts @@ -13,6 +13,8 @@ export type Maybe = T | null | undefined; export type NonMutable = T extends object ? Readonly : T; export type AnyRecord = Record; +export type UnknownRecord = Record; +export type ReadonlyRecord = Readonly>; export type AnyComponent = ComponentType; diff --git a/packages/react-native-reanimated/src/updateProps/updateProps.ts b/packages/react-native-reanimated/src/updateProps/updateProps.ts index 3f5085c5f16c..40b012e9a6ab 100644 --- a/packages/react-native-reanimated/src/updateProps/updateProps.ts +++ b/packages/react-native-reanimated/src/updateProps/updateProps.ts @@ -6,18 +6,11 @@ import { scheduleOnRN, scheduleOnUI } from 'react-native-worklets'; import { IS_JEST, - processBoxShadowNative, - processColorsInProps, - processFilter, - processTransform, ReanimatedError, SHOULD_BE_USE_WEB, + styleBuilder, } from '../common'; -import { - processBoxShadowWeb, - processFilterWeb, - processTransformOrigin, -} from '../common/web'; +import { processBoxShadowWeb, processFilterWeb } from '../common/web'; import type { AnimatedStyle, ShadowNodeWrapper, @@ -55,24 +48,10 @@ if (SHOULD_BE_USE_WEB) { } else { updateProps = (viewDescriptors, updates) => { 'worklet'; - /* TODO: Improve this config structure in the future - * The goal is to create a simplified version of `src/css/platform/native/config.ts`, - * containing only properties that require processing and their associated processors - * */ - processColorsInProps(updates); - if ('transformOrigin' in updates) { - updates.transformOrigin = processTransformOrigin(updates.transformOrigin); - } - if ('transform' in updates) { - updates.transform = processTransform(updates.transform); - } - if ('boxShadow' in updates) { - updates.boxShadow = processBoxShadowNative(updates.boxShadow); - } - if ('filter' in updates) { - updates.filter = processFilter(updates.filter); - } - global.UpdatePropsManager.update(viewDescriptors, updates); + global.UpdatePropsManager.update( + viewDescriptors, + styleBuilder.build(updates) + ); }; } From f3660a218bf296e58554828db95e4992a724b95d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81opaci=C5=84ski?= Date: Wed, 3 Dec 2025 03:12:20 +0000 Subject: [PATCH 2/7] Rename style builder to props builder --- .../src/common/style/config.ts | 4 +- ...eStyleBuilder.ts => createPropsBuilder.ts} | 2 +- .../common/style/createStyleBuilder-old.ts | 26 ++++----- .../src/common/style/index.ts | 5 +- .../src/common/style/propsBuilder.ts | 5 +- .../src/common/style/types.ts | 10 ++-- .../src/css/native/__tests__/registry.test.ts | 56 +++++++++---------- .../native/keyframes/CSSKeyframesRuleImpl.ts | 4 +- .../src/css/native/managers/CSSManager.ts | 14 ++--- .../animation/__tests__/animationName.test.ts | 30 +++++----- .../animation/__tests__/keyframes.test.ts | 36 ++++++------ .../normalization/animation/keyframes.ts | 8 +-- .../src/css/native/registry.ts | 30 +++++----- .../src/css/svg/init.ts | 12 ++-- .../src/updateProps/updateProps.ts | 4 +- 15 files changed, 123 insertions(+), 123 deletions(-) rename packages/react-native-reanimated/src/common/style/{createStyleBuilder.ts => createPropsBuilder.ts} (98%) diff --git a/packages/react-native-reanimated/src/common/style/config.ts b/packages/react-native-reanimated/src/common/style/config.ts index 9c3dca648cdf..f0e99f50c868 100644 --- a/packages/react-native-reanimated/src/common/style/config.ts +++ b/packages/react-native-reanimated/src/common/style/config.ts @@ -14,11 +14,11 @@ import { processTransform, processTransformOrigin, } from './processors'; -import type { StyleBuilderConfig } from './types'; +import type { PropsBuilderConfig } from './types'; const colorAttributes = { process: processColor }; -export const BASE_PROPERTIES_CONFIG: StyleBuilderConfig = { +export const BASE_PROPERTIES_CONFIG: PropsBuilderConfig = { /** Layout and Positioning */ // FLEXBOX flex: true, diff --git a/packages/react-native-reanimated/src/common/style/createStyleBuilder.ts b/packages/react-native-reanimated/src/common/style/createPropsBuilder.ts similarity index 98% rename from packages/react-native-reanimated/src/common/style/createStyleBuilder.ts rename to packages/react-native-reanimated/src/common/style/createPropsBuilder.ts index 8f42dbfada4e..0e24db9d57d2 100644 --- a/packages/react-native-reanimated/src/common/style/createStyleBuilder.ts +++ b/packages/react-native-reanimated/src/common/style/createPropsBuilder.ts @@ -30,7 +30,7 @@ export default function createPropsBuilder({ do { if (++depth > MAX_PROCESS_DEPTH) { throw new ReanimatedError( - `Max process depth for style builder reached for property ${key}` + `Max process depth for props builder reached for property ${key}` ); } processedEntry = processConfigEntry({ diff --git a/packages/react-native-reanimated/src/common/style/createStyleBuilder-old.ts b/packages/react-native-reanimated/src/common/style/createStyleBuilder-old.ts index 991bcd2180b8..7ea8037e271b 100644 --- a/packages/react-native-reanimated/src/common/style/createStyleBuilder-old.ts +++ b/packages/react-native-reanimated/src/common/style/createStyleBuilder-old.ts @@ -4,26 +4,26 @@ import type { AnyRecord, ValueProcessorContext } from '../types'; import { ValueProcessorTarget } from '../types'; import { isConfigPropertyAlias, isDefined, isRecord } from '../utils'; import type { - StyleBuilder, - StyleBuilderConfig, - StyleBuildMiddleware, + PropsBuilder, + PropsBuilderConfig, + PropsBuildMiddleware, } from './types'; -type StyleBuilderOptions

= { - buildMiddleware?: StyleBuildMiddleware

; +type PropsBuilderOptions

= { + buildMiddleware?: PropsBuildMiddleware

; separatelyInterpolatedNestedProperties?: (keyof P)[]; target?: ValueProcessorTarget; }; -class StyleBuilderImpl

implements StyleBuilder

{ - private readonly buildMiddleware: StyleBuildMiddleware

; - private readonly config: StyleBuilderConfig

; +class PropsBuilderImpl

implements PropsBuilder

{ + private readonly buildMiddleware: PropsBuildMiddleware

; + private readonly config: PropsBuilderConfig

; private readonly separatelyInterpolatedNestedProperties_: (keyof P)[]; private readonly context: ValueProcessorContext; private processedProps = {} as P; - constructor(config: StyleBuilderConfig

, options?: StyleBuilderOptions

) { + constructor(config: PropsBuilderConfig

, options?: PropsBuilderOptions

) { this.config = config; this.buildMiddleware = options?.buildMiddleware ?? ((props) => props); this.separatelyInterpolatedNestedProperties_ = @@ -96,8 +96,8 @@ class StyleBuilderImpl

implements StyleBuilder

{ } export default function createStyleBuilder

( - config: StyleBuilderConfig

, - options?: StyleBuilderOptions

-): StyleBuilder> { - return new StyleBuilderImpl(config, options); + config: PropsBuilderConfig

, + options?: PropsBuilderOptions

+): PropsBuilder> { + return new PropsBuilderImpl(config, options); } diff --git a/packages/react-native-reanimated/src/common/style/index.ts b/packages/react-native-reanimated/src/common/style/index.ts index 70db2e652fde..dbd33a3d6a2e 100644 --- a/packages/react-native-reanimated/src/common/style/index.ts +++ b/packages/react-native-reanimated/src/common/style/index.ts @@ -2,6 +2,7 @@ export * from './config'; export { default as createStyleBuilder } from './createStyleBuilder-old'; export * from './processors'; -export { default as styleBuilder } from './propsBuilder'; -export type { StyleBuilder } from './types'; +export { default as propsBuilder } from './propsBuilder'; +export type { PropsBuilder, PropsBuilderConfig, PropsBuildMiddleware } from './types'; +export type { PropsBuilder as StyleBuilder } from './types'; export type * from './types'; diff --git a/packages/react-native-reanimated/src/common/style/propsBuilder.ts b/packages/react-native-reanimated/src/common/style/propsBuilder.ts index 378a72de43a2..6b46b7861d06 100644 --- a/packages/react-native-reanimated/src/common/style/propsBuilder.ts +++ b/packages/react-native-reanimated/src/common/style/propsBuilder.ts @@ -2,15 +2,14 @@ import type { ValueProcessor } from '../types'; import { isConfigPropertyAlias, isRecord } from '../utils'; import { BASE_PROPERTIES_CONFIG } from './config'; -import createStyleBuilder from './createStyleBuilder'; +import createPropsBuilder from './createPropsBuilder'; const hasValueProcessor = ( configValue: unknown ): configValue is { process: ValueProcessor } => isRecord(configValue) && 'process' in configValue; -// TODO - maybe rename to propsBuilder, as we have updateProps -const propsBuilder = createStyleBuilder({ +const propsBuilder = createPropsBuilder({ config: BASE_PROPERTIES_CONFIG, processConfigEntry: ({ configValue, config }) => { if (configValue === true) { diff --git a/packages/react-native-reanimated/src/common/style/types.ts b/packages/react-native-reanimated/src/common/style/types.ts index f5e1896b15ac..3a00066e560c 100644 --- a/packages/react-native-reanimated/src/common/style/types.ts +++ b/packages/react-native-reanimated/src/common/style/types.ts @@ -1,9 +1,9 @@ 'use strict'; import type { AnyRecord, ConfigPropertyAlias, ValueProcessor } from '../types'; -export type StyleBuildMiddleware

= (props: P) => P; +export type PropsBuildMiddleware

= (props: P) => P; -export type StyleBuilder

= { +export type PropsBuilder

= { isSeparatelyInterpolatedNestedProperty(property: keyof P): boolean; add(property: keyof P, value: P[keyof P]): void; buildFrom(props: P): P | null; @@ -13,7 +13,7 @@ type PropertyValueConfigBase

= | boolean // true - included, false - excluded | ConfigPropertyAlias

; // alias for another property -type StyleBuilderPropertyConfig< +type PropsBuilderPropertyConfig< P extends AnyRecord, K extends keyof P = keyof P, > = @@ -25,6 +25,6 @@ type StyleBuilderPropertyConfig< process: ValueProcessor[K], any>; // for custom value processing }; -export type StyleBuilderConfig

= { - [K in keyof Required

]: StyleBuilderPropertyConfig; +export type PropsBuilderConfig

= { + [K in keyof Required

]: PropsBuilderPropertyConfig; }; diff --git a/packages/react-native-reanimated/src/css/native/__tests__/registry.test.ts b/packages/react-native-reanimated/src/css/native/__tests__/registry.test.ts index dc82813b0d39..e034196a4cee 100644 --- a/packages/react-native-reanimated/src/css/native/__tests__/registry.test.ts +++ b/packages/react-native-reanimated/src/css/native/__tests__/registry.test.ts @@ -2,76 +2,76 @@ import { BASE_PROPERTIES_CONFIG } from '../../../common'; import { ERROR_MESSAGES, - getStyleBuilder, - hasStyleBuilder, - registerComponentStyleBuilder, + getPropsBuilder, + hasPropsBuilder, + registerComponentPropsBuilder, } from '../registry'; describe('registry', () => { - describe('hasStyleBuilder', () => { + describe('hasPropsBuilder', () => { test('returns true for registered component names', () => { const componentName = 'CustomComponent'; const config = { width: true, height: true }; - registerComponentStyleBuilder(componentName, config); + registerComponentPropsBuilder(componentName, config); - expect(hasStyleBuilder(componentName)).toBe(true); + expect(hasPropsBuilder(componentName)).toBe(true); }); test('returns true for RCT prefixed component names', () => { - expect(hasStyleBuilder('RCTView')).toBe(true); - expect(hasStyleBuilder('RCTText')).toBe(true); + expect(hasPropsBuilder('RCTView')).toBe(true); + expect(hasPropsBuilder('RCTText')).toBe(true); }); test('returns false for unregistered component names', () => { - expect(hasStyleBuilder('UnregisteredComponent')).toBe(false); + expect(hasPropsBuilder('UnregisteredComponent')).toBe(false); }); }); - describe('getStyleBuilder', () => { - test('returns registered style builder for custom component', () => { + describe('getPropsBuilder', () => { + test('returns registered props builder for custom component', () => { const componentName = 'CustomComponent'; const config = { width: true, height: true }; - registerComponentStyleBuilder(componentName, config); - const styleBuilder = getStyleBuilder(componentName); + registerComponentPropsBuilder(componentName, config); + const propsBuilder = getPropsBuilder(componentName); - expect(styleBuilder).toBeDefined(); - expect(typeof styleBuilder.buildFrom).toBe('function'); + expect(propsBuilder).toBeDefined(); + expect(typeof propsBuilder.buildFrom).toBe('function'); }); - test('returns base style builder for RCT prefixed components', () => { - const styleBuilder = getStyleBuilder('RCTView'); + test('returns base props builder for RCT prefixed components', () => { + const propsBuilder = getPropsBuilder('RCTView'); - expect(styleBuilder).toBeDefined(); - expect(typeof styleBuilder.buildFrom).toBe('function'); + expect(propsBuilder).toBeDefined(); + expect(typeof propsBuilder.buildFrom).toBe('function'); }); test('throws error for unregistered component names', () => { expect(() => { - getStyleBuilder('UnregisteredComponent'); - }).toThrow(ERROR_MESSAGES.styleBuilderNotFound('UnregisteredComponent')); + getPropsBuilder('UnregisteredComponent'); + }).toThrow(ERROR_MESSAGES.propsBuilderNotFound('UnregisteredComponent')); }); }); - describe('registerComponentStyleBuilder', () => { + describe('registerComponentPropsBuilder', () => { test('registers a style builder', () => { const componentName = 'TestComponent'; const config = { width: true, height: true }; - registerComponentStyleBuilder(componentName, config); + registerComponentPropsBuilder(componentName, config); - expect(hasStyleBuilder(componentName)).toBe(true); - expect(getStyleBuilder(componentName)).toBeDefined(); + expect(hasPropsBuilder(componentName)).toBe(true); + expect(getPropsBuilder(componentName)).toBeDefined(); }); test('works with base properties config', () => { const componentName = 'BaseConfigComponent'; - registerComponentStyleBuilder(componentName, BASE_PROPERTIES_CONFIG); + registerComponentPropsBuilder(componentName, BASE_PROPERTIES_CONFIG); - expect(hasStyleBuilder(componentName)).toBe(true); - expect(getStyleBuilder(componentName)).toBeDefined(); + expect(hasPropsBuilder(componentName)).toBe(true); + expect(getPropsBuilder(componentName)).toBeDefined(); }); }); }); diff --git a/packages/react-native-reanimated/src/css/native/keyframes/CSSKeyframesRuleImpl.ts b/packages/react-native-reanimated/src/css/native/keyframes/CSSKeyframesRuleImpl.ts index 2e5813c4b62a..c4cb7be2eb88 100644 --- a/packages/react-native-reanimated/src/css/native/keyframes/CSSKeyframesRuleImpl.ts +++ b/packages/react-native-reanimated/src/css/native/keyframes/CSSKeyframesRuleImpl.ts @@ -3,7 +3,7 @@ import type { PlainStyle } from '../../../common'; import { CSSKeyframesRuleBase } from '../../models'; import type { CSSAnimationKeyframes } from '../../types'; import { normalizeAnimationKeyframes } from '../normalization'; -import { getStyleBuilder } from '../registry'; +import { getPropsBuilder } from '../registry'; import type { NormalizedCSSAnimationKeyframesConfig } from '../types'; export default class CSSKeyframesRuleImpl< @@ -24,7 +24,7 @@ export default class CSSKeyframesRuleImpl< if (!this.normalizedKeyframesCache_[viewName]) { this.normalizedKeyframesCache_[viewName] = normalizeAnimationKeyframes( this.cssRules, - getStyleBuilder(viewName) + getPropsBuilder(viewName) ); } diff --git a/packages/react-native-reanimated/src/css/native/managers/CSSManager.ts b/packages/react-native-reanimated/src/css/native/managers/CSSManager.ts index 6c1b9f4d8bb2..831e31892af7 100644 --- a/packages/react-native-reanimated/src/css/native/managers/CSSManager.ts +++ b/packages/react-native-reanimated/src/css/native/managers/CSSManager.ts @@ -1,14 +1,14 @@ 'use strict'; import type { AnyRecord } from '../../../common'; import { ReanimatedError } from '../../../common'; -import type { StyleBuilder } from '../../../common/style'; +import type { PropsBuilder } from '../../../common/style'; import type { ShadowNodeWrapper } from '../../../commonTypes'; import type { ViewInfo } from '../../../createAnimatedComponent/commonTypes'; import type { CSSStyle } from '../../types'; import type { ICSSManager } from '../../types/interfaces'; import { filterCSSAndStyleProperties } from '../../utils'; import { setViewStyle } from '../proxy'; -import { getStyleBuilder, hasStyleBuilder } from '../registry'; +import { getPropsBuilder, hasPropsBuilder } from '../registry'; import CSSAnimationsManager from './CSSAnimationsManager'; import CSSTransitionsManager from './CSSTransitionsManager'; @@ -17,7 +17,7 @@ export default class CSSManager implements ICSSManager { private readonly cssTransitionsManager: CSSTransitionsManager; private readonly viewTag: number; private readonly viewName: string; - private readonly styleBuilder: StyleBuilder | null = null; + private readonly propsBuilder: PropsBuilder | null = null; private isFirstUpdate: boolean = true; constructor({ shadowNodeWrapper, viewTag, viewName = 'RCTView' }: ViewInfo) { @@ -25,8 +25,8 @@ export default class CSSManager implements ICSSManager { const wrapper = shadowNodeWrapper as ShadowNodeWrapper; this.viewName = viewName; - this.styleBuilder = hasStyleBuilder(viewName) - ? getStyleBuilder(viewName) + this.propsBuilder = hasPropsBuilder(viewName) + ? getPropsBuilder(viewName) : null; this.cssAnimationsManager = new CSSAnimationsManager( wrapper, @@ -40,13 +40,13 @@ export default class CSSManager implements ICSSManager { const [animationProperties, transitionProperties, filteredStyle] = filterCSSAndStyleProperties(style); - if (!this.styleBuilder && (animationProperties || transitionProperties)) { + if (!this.propsBuilder && (animationProperties || transitionProperties)) { throw new ReanimatedError( `Tried to apply CSS animations to ${this.viewName} which is not supported` ); } - const normalizedStyle = this.styleBuilder?.buildFrom(filteredStyle); + const normalizedStyle = this.propsBuilder?.buildFrom(filteredStyle); // If the update is called during the first css style update, we won't // trigger CSS transitions and set styles before attaching CSS transitions diff --git a/packages/react-native-reanimated/src/css/native/normalization/animation/__tests__/animationName.test.ts b/packages/react-native-reanimated/src/css/native/normalization/animation/__tests__/animationName.test.ts index 2be3af317edd..47ca343b7686 100644 --- a/packages/react-native-reanimated/src/css/native/normalization/animation/__tests__/animationName.test.ts +++ b/packages/react-native-reanimated/src/css/native/normalization/animation/__tests__/animationName.test.ts @@ -1,11 +1,11 @@ 'use strict'; import { ReanimatedError } from '../../../../../common'; import type { CSSAnimationKeyframeSelector } from '../../../../types'; -import { getStyleBuilder } from '../../../registry'; +import { getPropsBuilder } from '../../../registry'; import { ERROR_MESSAGES, normalizeAnimationKeyframes } from '../keyframes'; describe(normalizeAnimationKeyframes, () => { - const styleBuilder = getStyleBuilder('RCTView'); // Must be a valid view name + const propsBuilder = getPropsBuilder('RCTView'); // Must be a valid view name describe('offset normalization', () => { describe('when offset is valid', () => { @@ -22,7 +22,7 @@ describe(normalizeAnimationKeyframes, () => { expect( normalizeAnimationKeyframes( { [offset]: { opacity: 1 } }, - styleBuilder + propsBuilder ) ).toEqual({ keyframesStyle: { opacity: [{ offset: expected, value: 1 }] }, @@ -39,7 +39,7 @@ describe(normalizeAnimationKeyframes, () => { expect(() => normalizeAnimationKeyframes( { [value]: { opacity: 1 } }, - styleBuilder + propsBuilder ) ).toThrow( new ReanimatedError(ERROR_MESSAGES.invalidOffsetType(value)) @@ -56,7 +56,7 @@ describe(normalizeAnimationKeyframes, () => { expect(() => normalizeAnimationKeyframes( { [value]: { opacity: 1 } }, - styleBuilder + propsBuilder ) ).toThrow( new ReanimatedError(ERROR_MESSAGES.invalidOffsetRange(value)) @@ -78,7 +78,7 @@ describe(normalizeAnimationKeyframes, () => { expect( normalizeAnimationKeyframes( { [offset]: { opacity: 1 } }, - styleBuilder + propsBuilder ) ).toEqual({ keyframeTimingFunctions: {}, @@ -100,7 +100,7 @@ describe(normalizeAnimationKeyframes, () => { ])('throws an error for %p', (offset, errorMsg) => { const value = offset as CSSAnimationKeyframeSelector; expect(() => - normalizeAnimationKeyframes({ [value]: { opacity: 1 } }, styleBuilder) + normalizeAnimationKeyframes({ [value]: { opacity: 1 } }, propsBuilder) ).toThrow(new ReanimatedError(errorMsg)); }); }); @@ -115,7 +115,7 @@ describe(normalizeAnimationKeyframes, () => { '50%': { opacity: 0.5 }, to: { opacity: 1 }, }, - styleBuilder + propsBuilder ) ).toEqual({ keyframesStyle: { @@ -136,7 +136,7 @@ describe(normalizeAnimationKeyframes, () => { from: { shadowOffset: { width: 0, height: 0 } }, to: { shadowOffset: { width: 10, height: 10 } }, }, - styleBuilder + propsBuilder ) ).toEqual({ keyframesStyle: { @@ -165,7 +165,7 @@ describe(normalizeAnimationKeyframes, () => { '25%': { opacity: 0.25 }, from: { opacity: 0 }, }, - styleBuilder + propsBuilder ) ).toEqual({ keyframesStyle: { @@ -188,7 +188,7 @@ describe(normalizeAnimationKeyframes, () => { from: { transform: [{ scale: 0 }, { rotate: '0deg' }] }, to: { transform: [{ scale: 1 }, { rotate: '360deg' }] }, }, - styleBuilder + propsBuilder ) ).toEqual({ keyframesStyle: { @@ -208,7 +208,7 @@ describe(normalizeAnimationKeyframes, () => { from: { opacity: 0, transform: undefined }, to: { opacity: 1 }, }, - styleBuilder + propsBuilder ) ).toEqual({ keyframesStyle: { @@ -229,7 +229,7 @@ describe(normalizeAnimationKeyframes, () => { '50%': { opacity: 0.5 }, to: {}, }, - styleBuilder + propsBuilder ) ).toEqual({ keyframesStyle: { @@ -250,7 +250,7 @@ describe(normalizeAnimationKeyframes, () => { '50%': { opacity: 0.75 }, '75%': { opacity: 1, animationTimingFunction: 'ease-out' }, }, - styleBuilder + propsBuilder ) ).toEqual({ keyframesStyle: { @@ -281,7 +281,7 @@ describe(normalizeAnimationKeyframes, () => { { '0%, 100%': { opacity: 0, animationTimingFunction: 'ease-in' }, }, - styleBuilder + propsBuilder ) ).toEqual({ keyframesStyle: { diff --git a/packages/react-native-reanimated/src/css/native/normalization/animation/__tests__/keyframes.test.ts b/packages/react-native-reanimated/src/css/native/normalization/animation/__tests__/keyframes.test.ts index f624585aeb2b..c878296c2398 100644 --- a/packages/react-native-reanimated/src/css/native/normalization/animation/__tests__/keyframes.test.ts +++ b/packages/react-native-reanimated/src/css/native/normalization/animation/__tests__/keyframes.test.ts @@ -1,7 +1,7 @@ 'use strict'; import { ReanimatedError } from '../../../../../common'; import type { Repeat } from '../../../../types'; -import { getStyleBuilder } from '../../../registry'; +import { getPropsBuilder } from '../../../registry'; import { ERROR_MESSAGES, normalizeAnimationKeyframes, @@ -82,7 +82,7 @@ describe(normalizeKeyframeSelector, () => { }); }); -function mockStyleBuilder( +function mockPropsBuilder( separatelyInterpolatedNestedProperties: string[] = [] ) { const separatelyInterpolatedNestedPropertiesSet = new Set( @@ -103,7 +103,7 @@ function mockStyleBuilder( describe(processKeyframes, () => { describe('offset handling', () => { test('sorts keyframes and accepts percentages', () => { - const styleBuilder = mockStyleBuilder(); + const propsBuilder = mockPropsBuilder(); const keyframes = { '75%': { opacity: 0.75 }, from: { opacity: 0 }, @@ -111,7 +111,7 @@ describe(processKeyframes, () => { to: { opacity: 1 }, }; - expect(processKeyframes(keyframes, styleBuilder)).toEqual([ + expect(processKeyframes(keyframes, propsBuilder)).toEqual([ { offset: 0, style: { opacity: 0 } }, { offset: 0.25, style: { opacity: 0.25 } }, { offset: 0.75, style: { opacity: 0.75 } }, @@ -120,13 +120,13 @@ describe(processKeyframes, () => { }); test('splits multi-selector entries into separate keyframes', () => { - const styleBuilder = mockStyleBuilder(); + const propsBuilder = mockPropsBuilder(); const keyframes = { 'from, 50%': { opacity: 0.5 }, to: { opacity: 1 }, }; - expect(processKeyframes(keyframes, styleBuilder)).toEqual([ + expect(processKeyframes(keyframes, propsBuilder)).toEqual([ { offset: 0, style: { opacity: 0.5 } }, { offset: 0.5, style: { opacity: 0.5 } }, { offset: 1, style: { opacity: 1 } }, @@ -141,7 +141,7 @@ describe(processKeyframes, () => { '100%': { transform: [{ translateX: 100 }] }, }; - expect(processKeyframes(keyframes, getStyleBuilder('RCTView'))).toEqual([ + expect(processKeyframes(keyframes, getPropsBuilder('RCTView'))).toEqual([ { offset: 0, style: { transform: [{ translateX: 0 }] } }, { offset: 1, style: { transform: [{ translateX: 100 }] } }, ]); @@ -155,7 +155,7 @@ describe(processKeyframes, () => { to: { transformOrigin: toTransformOrigin }, }; - const result = processKeyframes(keyframes, getStyleBuilder('RCTView')); + const result = processKeyframes(keyframes, getPropsBuilder('RCTView')); expect(result).toEqual([ { @@ -178,7 +178,7 @@ describe(processKeyframes, () => { to: { [property]: { width: 10, height: 5 } }, }; - const result = processKeyframes(keyframes, getStyleBuilder('RCTView')); + const result = processKeyframes(keyframes, getPropsBuilder('RCTView')); expect(result).toEqual([ { @@ -223,7 +223,7 @@ describe(processKeyframes, () => { }, }; - const result = processKeyframes(keyframes, getStyleBuilder('RCTView')); + const result = processKeyframes(keyframes, getPropsBuilder('RCTView')); expect(result).toEqual([ { @@ -280,8 +280,8 @@ describe(processKeyframes, () => { }); test('drops keyframes when processed style is undefined', () => { - const styleBuilder = mockStyleBuilder(['shadowOffset']); - styleBuilder.buildFrom + const propsBuilder = mockPropsBuilder(['shadowOffset']); + propsBuilder.buildFrom .mockImplementationOnce(() => undefined) .mockImplementation((style) => style); @@ -290,20 +290,20 @@ describe(processKeyframes, () => { to: { shadowOffset: { width: 10, height: 5 } }, }; - expect(processKeyframes(keyframes, styleBuilder)).toEqual([ + expect(processKeyframes(keyframes, propsBuilder)).toEqual([ { offset: 1, style: { shadowOffset: { width: 10, height: 5 } } }, ]); }); test('merges styles for duplicate offsets', () => { - const styleBuilder = mockStyleBuilder(); + const propsBuilder = mockPropsBuilder(); const keyframes = { '0%': { opacity: 0.5 }, '0': { transform: [{ scale: 1 }] }, '100%': { opacity: 1 }, }; - expect(processKeyframes(keyframes, styleBuilder)).toEqual([ + expect(processKeyframes(keyframes, propsBuilder)).toEqual([ { offset: 0, style: { opacity: 0.5, transform: [{ scale: 1 }] }, @@ -314,7 +314,7 @@ describe(processKeyframes, () => { }); describe(normalizeAnimationKeyframes, () => { - const styleBuilder = getStyleBuilder('RCTView'); + const propsBuilder = getPropsBuilder('RCTView'); test('aggregates styles and timing functions across keyframes', () => { const result = normalizeAnimationKeyframes( @@ -326,7 +326,7 @@ describe(normalizeAnimationKeyframes, () => { }, to: { opacity: 1 }, }, - styleBuilder + propsBuilder ); expect(result).toEqual({ @@ -353,7 +353,7 @@ describe(normalizeAnimationKeyframes, () => { from: { opacity: 0, animationTimingFunction: 'ease-in' }, to: { opacity: 1, animationTimingFunction: 'ease-out' }, }, - styleBuilder + propsBuilder ); expect(result).toEqual({ diff --git a/packages/react-native-reanimated/src/css/native/normalization/animation/keyframes.ts b/packages/react-native-reanimated/src/css/native/normalization/animation/keyframes.ts index 4714f7eb1d54..af9566dfd116 100644 --- a/packages/react-native-reanimated/src/css/native/normalization/animation/keyframes.ts +++ b/packages/react-native-reanimated/src/css/native/normalization/animation/keyframes.ts @@ -1,5 +1,5 @@ 'use strict'; -import type { AnyRecord, StyleBuilder } from '../../../../common'; +import type { AnyRecord, PropsBuilder } from '../../../../common'; import { isDefined, isNumber, ReanimatedError } from '../../../../common'; import type { StyleProps } from '../../../../commonTypes'; import { PERCENTAGE_REGEX } from '../../../constants'; @@ -67,7 +67,7 @@ type ProcessedKeyframes = Array<{ export function processKeyframes( keyframes: CSSAnimationKeyframes, - styleBuilder: StyleBuilder + styleBuilder: PropsBuilder ): ProcessedKeyframes { return Object.entries(keyframes) .flatMap( @@ -102,7 +102,7 @@ function processStyleProperties( offset: number, style: S, keyframeStyle: AnyRecord, - styleBuilder: StyleBuilder + styleBuilder: PropsBuilder ) { Object.entries(style).forEach(([property, value]) => { if (!isDefined(value)) { @@ -133,7 +133,7 @@ function processStyleProperties( export function normalizeAnimationKeyframes( keyframes: CSSAnimationKeyframes, - styleBuilder: StyleBuilder + styleBuilder: PropsBuilder ): NormalizedCSSAnimationKeyframesConfig { const keyframesStyle: NormalizedCSSKeyframesStyle = {}; const timingFunctions: NormalizedCSSKeyframeTimingFunctions = {}; diff --git a/packages/react-native-reanimated/src/css/native/registry.ts b/packages/react-native-reanimated/src/css/native/registry.ts index f7cacf62fe25..49eee6f3350e 100644 --- a/packages/react-native-reanimated/src/css/native/registry.ts +++ b/packages/react-native-reanimated/src/css/native/registry.ts @@ -1,5 +1,5 @@ 'use strict'; -import type { StyleBuilder, StyleBuilderConfig } from '../../common'; +import type { PropsBuilder, StyleBuilderConfig } from '../../common'; import { BASE_PROPERTIES_CONFIG, createStyleBuilder, @@ -8,11 +8,11 @@ import { } from '../../common'; export const ERROR_MESSAGES = { - styleBuilderNotFound: (componentName: string) => - `CSS style builder for component ${componentName} was not found`, + propsBuilderNotFound: (componentName: string) => + `CSS props builder for component ${componentName} was not found`, }; -const baseStyleBuilder = createStyleBuilder(BASE_PROPERTIES_CONFIG, { +const basePropsBuilder = createStyleBuilder(BASE_PROPERTIES_CONFIG, { separatelyInterpolatedNestedProperties: [ 'boxShadow', 'shadowOffset', @@ -22,32 +22,32 @@ const baseStyleBuilder = createStyleBuilder(BASE_PROPERTIES_CONFIG, { target: ValueProcessorTarget.CSS, }); -const STYLE_BUILDERS: Record = {}; +const PROPS_BUILDERS: Record = {}; -export function hasStyleBuilder(componentName: string): boolean { - return !!STYLE_BUILDERS[componentName] || componentName.startsWith('RCT'); +export function hasPropsBuilder(componentName: string): boolean { + return !!PROPS_BUILDERS[componentName] || componentName.startsWith('RCT'); } -export function getStyleBuilder(componentName: string): StyleBuilder { - const styleBuilder = STYLE_BUILDERS[componentName]; +export function getPropsBuilder(componentName: string): PropsBuilder { + const propsBuilder = PROPS_BUILDERS[componentName]; - if (styleBuilder) { - return styleBuilder; + if (propsBuilder) { + return propsBuilder; } // This captures all React Native components if (componentName.startsWith('RCT')) { - return baseStyleBuilder; + return basePropsBuilder; } - throw new ReanimatedError(ERROR_MESSAGES.styleBuilderNotFound(componentName)); + throw new ReanimatedError(ERROR_MESSAGES.propsBuilderNotFound(componentName)); } -export function registerComponentStyleBuilder( +export function registerComponentPropsBuilder( componentName: string, config: StyleBuilderConfig ) { - STYLE_BUILDERS[componentName] = createStyleBuilder(config, { + PROPS_BUILDERS[componentName] = createStyleBuilder(config, { target: ValueProcessorTarget.CSS, }); } diff --git a/packages/react-native-reanimated/src/css/svg/init.ts b/packages/react-native-reanimated/src/css/svg/init.ts index c3fe3b53ca8b..7d92bd13ebb3 100644 --- a/packages/react-native-reanimated/src/css/svg/init.ts +++ b/packages/react-native-reanimated/src/css/svg/init.ts @@ -1,5 +1,5 @@ 'use strict'; -import { registerComponentStyleBuilder } from '../native'; +import { registerComponentPropsBuilder } from '../native'; import { SVG_CIRCLE_PROPERTIES_CONFIG, SVG_ELLIPSE_PROPERTIES_CONFIG, @@ -9,11 +9,11 @@ import { } from './native'; export function initSvgCssSupport() { - registerComponentStyleBuilder('RNSVGCircle', SVG_CIRCLE_PROPERTIES_CONFIG); - registerComponentStyleBuilder('RNSVGEllipse', SVG_ELLIPSE_PROPERTIES_CONFIG); - registerComponentStyleBuilder('RNSVGLine', SVG_LINE_PROPERTIES_CONFIG); - registerComponentStyleBuilder('RNSVGPath', SVG_PATH_PROPERTIES_CONFIG); - registerComponentStyleBuilder('RNSVGRect', SVG_RECT_PROPERTIES_CONFIG); + registerComponentPropsBuilder('RNSVGCircle', SVG_CIRCLE_PROPERTIES_CONFIG); + registerComponentPropsBuilder('RNSVGEllipse', SVG_ELLIPSE_PROPERTIES_CONFIG); + registerComponentPropsBuilder('RNSVGLine', SVG_LINE_PROPERTIES_CONFIG); + registerComponentPropsBuilder('RNSVGPath', SVG_PATH_PROPERTIES_CONFIG); + registerComponentPropsBuilder('RNSVGRect', SVG_RECT_PROPERTIES_CONFIG); // TODO: Add more SVG components as they are implemented } diff --git a/packages/react-native-reanimated/src/updateProps/updateProps.ts b/packages/react-native-reanimated/src/updateProps/updateProps.ts index 40b012e9a6ab..6bb823ad6549 100644 --- a/packages/react-native-reanimated/src/updateProps/updateProps.ts +++ b/packages/react-native-reanimated/src/updateProps/updateProps.ts @@ -8,7 +8,7 @@ import { IS_JEST, ReanimatedError, SHOULD_BE_USE_WEB, - styleBuilder, + propsBuilder, } from '../common'; import { processBoxShadowWeb, processFilterWeb } from '../common/web'; import type { @@ -50,7 +50,7 @@ if (SHOULD_BE_USE_WEB) { 'worklet'; global.UpdatePropsManager.update( viewDescriptors, - styleBuilder.build(updates) + propsBuilder.build(updates) ); }; } From ce25c4fd4788c8b18a7849b1227aa075542545b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81opaci=C5=84ski?= Date: Wed, 3 Dec 2025 03:57:34 +0000 Subject: [PATCH 3/7] Finish props builder build handler method --- .../src/common/style/createPropsBuilder.ts | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/packages/react-native-reanimated/src/common/style/createPropsBuilder.ts b/packages/react-native-reanimated/src/common/style/createPropsBuilder.ts index 0e24db9d57d2..ad55b004f68d 100644 --- a/packages/react-native-reanimated/src/common/style/createPropsBuilder.ts +++ b/packages/react-native-reanimated/src/common/style/createPropsBuilder.ts @@ -1,6 +1,7 @@ 'use strict'; import { ReanimatedError } from '../errors'; import type { ReadonlyRecord, UnknownRecord, ValueProcessor } from '../types'; +import { isRecord } from '../utils'; const MAX_PROCESS_DEPTH = 10; @@ -49,15 +50,37 @@ export default function createPropsBuilder({ return { build(props: Readonly, includeUndefined = false) { 'worklet'; - return buildProps( - Object.entries(props).reduce((acc, [key, value]) => { - const processedValue = processedConfig[key](value); // TODO - add value processor context - if (includeUndefined || processedValue !== undefined) { + const processed = Object.entries(props).reduce( + (acc, [key, value]) => { + const processor = processedConfig[key]; + + if (!processor) { + // Props is not supported, skip it + return acc; + } + + const processedValue = processor(value); + if (processedValue === undefined && !includeUndefined) { + // Skip if value is undefined and we don't want to include undefined values + return acc; + } + + if (isRecord(processedValue)) { + for (const processedKey in processedValue) { + if (!(processedKey in props)) { + acc[processedKey] = processedValue[processedKey]; + } + } + } else { acc[key] = processedValue; } + return acc; - }, {}) + }, + {} ); + + return buildProps(processed); }, }; } From 5fe518bdaeb842651083f66a1688ae5da5f2aec7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81opaci=C5=84ski?= Date: Wed, 3 Dec 2025 15:04:16 +0000 Subject: [PATCH 4/7] Some progress --- .../src/common/style/createPropsBuilder.ts | 101 ++++++++++------- .../common/style/createStyleBuilder-old.ts | 103 ------------------ .../src/common/style/index.ts | 7 +- .../src/common/style/propsBuilder.ts | 91 +++++++++++----- .../src/common/types/helpers.ts | 1 - .../src/css/native/registry.ts | 59 ++++++---- 6 files changed, 169 insertions(+), 193 deletions(-) delete mode 100644 packages/react-native-reanimated/src/common/style/createStyleBuilder-old.ts diff --git a/packages/react-native-reanimated/src/common/style/createPropsBuilder.ts b/packages/react-native-reanimated/src/common/style/createPropsBuilder.ts index ad55b004f68d..0a2b0c514648 100644 --- a/packages/react-native-reanimated/src/common/style/createPropsBuilder.ts +++ b/packages/react-native-reanimated/src/common/style/createPropsBuilder.ts @@ -1,55 +1,76 @@ 'use strict'; import { ReanimatedError } from '../errors'; -import type { ReadonlyRecord, UnknownRecord, ValueProcessor } from '../types'; +import type { + UnknownRecord, + ValueProcessor, + ValueProcessorContext, +} from '../types'; +import { ValueProcessorTarget } from '../types'; import { isRecord } from '../utils'; const MAX_PROCESS_DEPTH = 10; -type PropsBuilderConfig = { - config: ReadonlyRecord; +type CreatePropsBuilderParams = { + config: TPropsConfig; processConfigEntry: (params: { - configValue: TConfigValue; - config: ReadonlyRecord; - // ctx: TConfigContext; - }) => ValueProcessor | TConfigValue | undefined; - buildProps: (props: Readonly) => TBuildResult; + configValue: TPropsConfig[keyof TPropsConfig]; + config: TPropsConfig; + }) => ValueProcessor | TPropsConfig[keyof TPropsConfig] | undefined; + buildProps: (props: Readonly) => UnknownRecord; }; -//& { ctx: TConfigContext }; +type CreateStyleBuilderResult = { + build( + props: TProps, + options?: { + includeUndefined?: boolean; + target?: ValueProcessorTarget; + } + ): UnknownRecord; +} -export default function createPropsBuilder({ - config, +export default function createPropsBuilder({ processConfigEntry, - buildProps, -}: PropsBuilderConfig) { - const processedConfig = Object.entries(config).reduce< - Record unknown> - >((acc, [key, configValue]) => { - let processedEntry: ReturnType = configValue; - - let depth = 0; - do { - if (++depth > MAX_PROCESS_DEPTH) { - throw new ReanimatedError( - `Max process depth for props builder reached for property ${key}` - ); +config, +}: CreatePropsBuilderParams): CreateStyleBuilderResult { + const processedConfig = Object.entries(config).reduce< + Record + >((acc, [key, configValue]) => { + let processedEntry: ReturnType = configValue as TPropsConfig[keyof TPropsConfig]; + + let depth = 0; + do { + if (++depth > MAX_PROCESS_DEPTH) { + throw new ReanimatedError( + `Max process depth for props builder reached for property ${key}` + ); + } + processedEntry = processConfigEntry({ + configValue: processedEntry, + config, + }); + } while (processedEntry && typeof processedEntry !== 'function'); + + if (processedEntry) { + acc[key] = processedEntry as ValueProcessor; } - processedEntry = processConfigEntry({ - configValue: processedEntry, - config, - }); - } while (processedEntry && typeof processedEntry !== 'function'); - - if (processedEntry) { - acc[key] = processedEntry as ValueProcessor; - } - - return acc; - }, {}); - return { - build(props: Readonly, includeUndefined = false) { + return acc; + }, {}); + + return { + build( + props: Readonly, + { + includeUndefined = false, + target = ValueProcessorTarget.Default, + }: { + includeUndefined?: boolean; + target?: ValueProcessorTarget; + } = {} + ) { 'worklet'; + const context: ValueProcessorContext = { target }; const processed = Object.entries(props).reduce( (acc, [key, value]) => { const processor = processedConfig[key]; @@ -59,7 +80,7 @@ export default function createPropsBuilder({ return acc; } - const processedValue = processor(value); + const processedValue = processor(value, context); if (processedValue === undefined && !includeUndefined) { // Skip if value is undefined and we don't want to include undefined values return acc; @@ -80,7 +101,7 @@ export default function createPropsBuilder({ {} ); - return buildProps(processed); + return processed; }, }; } diff --git a/packages/react-native-reanimated/src/common/style/createStyleBuilder-old.ts b/packages/react-native-reanimated/src/common/style/createStyleBuilder-old.ts deleted file mode 100644 index 7ea8037e271b..000000000000 --- a/packages/react-native-reanimated/src/common/style/createStyleBuilder-old.ts +++ /dev/null @@ -1,103 +0,0 @@ -'use strict'; - -import type { AnyRecord, ValueProcessorContext } from '../types'; -import { ValueProcessorTarget } from '../types'; -import { isConfigPropertyAlias, isDefined, isRecord } from '../utils'; -import type { - PropsBuilder, - PropsBuilderConfig, - PropsBuildMiddleware, -} from './types'; - -type PropsBuilderOptions

= { - buildMiddleware?: PropsBuildMiddleware

; - separatelyInterpolatedNestedProperties?: (keyof P)[]; - target?: ValueProcessorTarget; -}; - -class PropsBuilderImpl

implements PropsBuilder

{ - private readonly buildMiddleware: PropsBuildMiddleware

; - private readonly config: PropsBuilderConfig

; - private readonly separatelyInterpolatedNestedProperties_: (keyof P)[]; - private readonly context: ValueProcessorContext; - - private processedProps = {} as P; - - constructor(config: PropsBuilderConfig

, options?: PropsBuilderOptions

) { - this.config = config; - this.buildMiddleware = options?.buildMiddleware ?? ((props) => props); - this.separatelyInterpolatedNestedProperties_ = - options?.separatelyInterpolatedNestedProperties ?? []; - this.context = { - target: options?.target ?? ValueProcessorTarget.Default, - }; - } - - isSeparatelyInterpolatedNestedProperty(property: keyof P): boolean { - return this.separatelyInterpolatedNestedProperties_.includes(property); - } - - add(property: keyof P, value: P[keyof P]): void { - const configValue = this.config[property]; - - if (!configValue || !isDefined(value)) { - return; - } - - if (configValue === true) { - this.maybeAssignProp(property, value); - } else if (isConfigPropertyAlias

(configValue)) { - this.add(configValue.as, value); - } else { - const { process } = configValue; - const processedValue = process ? process(value, this.context) : value; - - if (!isDefined(processedValue)) { - return; - } - - if (isRecord

(processedValue)) { - this.maybeAssignProps(processedValue); - } else { - this.maybeAssignProp(property, processedValue); - } - } - } - - build(): P | null { - const result = this.buildMiddleware(this.processedProps); - this.cleanup(); - - if (Object.keys(result).length === 0) { - return null; - } - - return result; - } - - buildFrom(props: P): P | null { - Object.entries(props).forEach(([key, value]) => this.add(key, value)); - return this.build(); - } - - private maybeAssignProp(property: keyof P, value: P[keyof P]) { - this.processedProps[property] ??= value; - } - - private maybeAssignProps(props: P) { - Object.entries(props).forEach(([key, value]) => - this.maybeAssignProp(key, value) - ); - } - - private cleanup() { - this.processedProps = {} as P; - } -} - -export default function createStyleBuilder

( - config: PropsBuilderConfig

, - options?: PropsBuilderOptions

-): PropsBuilder> { - return new PropsBuilderImpl(config, options); -} diff --git a/packages/react-native-reanimated/src/common/style/index.ts b/packages/react-native-reanimated/src/common/style/index.ts index dbd33a3d6a2e..6fe1f5837858 100644 --- a/packages/react-native-reanimated/src/common/style/index.ts +++ b/packages/react-native-reanimated/src/common/style/index.ts @@ -1,8 +1,5 @@ 'use strict'; export * from './config'; -export { default as createStyleBuilder } from './createStyleBuilder-old'; export * from './processors'; -export { default as propsBuilder } from './propsBuilder'; -export type { PropsBuilder, PropsBuilderConfig, PropsBuildMiddleware } from './types'; -export type { PropsBuilder as StyleBuilder } from './types'; -export type * from './types'; +export { default as createPropsBuilder } from './createPropsBuilder'; +export { default as propsBuilder, createNativePropsBuilder } from './propsBuilder'; diff --git a/packages/react-native-reanimated/src/common/style/propsBuilder.ts b/packages/react-native-reanimated/src/common/style/propsBuilder.ts index 6b46b7861d06..999c212e3942 100644 --- a/packages/react-native-reanimated/src/common/style/propsBuilder.ts +++ b/packages/react-native-reanimated/src/common/style/propsBuilder.ts @@ -1,5 +1,5 @@ 'use strict'; -import type { ValueProcessor } from '../types'; +import { ConfigPropertyAlias, PlainStyle, UnknownRecord, type ValueProcessor } from '../types'; import { isConfigPropertyAlias, isRecord } from '../utils'; import { BASE_PROPERTIES_CONFIG } from './config'; import createPropsBuilder from './createPropsBuilder'; @@ -9,29 +9,70 @@ const hasValueProcessor = ( ): configValue is { process: ValueProcessor } => isRecord(configValue) && 'process' in configValue; -const propsBuilder = createPropsBuilder({ - config: BASE_PROPERTIES_CONFIG, - processConfigEntry: ({ configValue, config }) => { - if (configValue === true) { - return (value) => { - 'worklet'; - return value; - }; - } - if (isConfigPropertyAlias(configValue)) { - return config[configValue.as]; - } - if (hasValueProcessor(configValue)) { - return (value) => { - 'worklet'; - return configValue.process(value); - }; - } - }, - buildProps: (props) => { - 'worklet'; - return props; - }, -}); +type PropsBuilderPropertyConfig< + TProps extends UnknownRecord = UnknownRecord, + K extends keyof TProps = keyof TProps, +> = + | boolean // true - included, false - excluded + | ConfigPropertyAlias // alias for another property + | { + // value can have any type as it is passed to CPP where we can expect a different + // type than in the React Native stylesheet (e.g. number for colors instead of string) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + process: ValueProcessor[K], any>; // for custom value processing + }; + +export function createNativePropsBuilder( + config: Required<{ [K in keyof TProps]: PropsBuilderPropertyConfig }> +) { + return createPropsBuilder({ + config, + processConfigEntry: ({ configValue, config }) => { + if (configValue === true) { + return (value) => { + 'worklet'; + return value; + }; + } + if (isConfigPropertyAlias(configValue)) { + return config[configValue.as]; + } + if (hasValueProcessor(configValue)) { + return (value, context) => { + 'worklet'; + return configValue.process(value, context); + }; + } + }, + }); +} + + + +// = createPropsBuilder({ +// processConfigEntry: ({ configValue, config }) => { +// if (configValue === true) { +// return (value) => { +// 'worklet'; +// return value; +// }; +// } +// if (isConfigPropertyAlias(configValue)) { +// return config[configValue.as]; +// } +// if (hasValueProcessor(configValue)) { +// return (value, context) => { +// 'worklet'; +// return configValue.process(value, context); +// }; +// } +// }, +// buildProps: (props) => { +// 'worklet'; +// return props; +// }, +// }); + +const propsBuilder = createNativePropsBuilder(BASE_PROPERTIES_CONFIG); export default propsBuilder; diff --git a/packages/react-native-reanimated/src/common/types/helpers.ts b/packages/react-native-reanimated/src/common/types/helpers.ts index 3e8e30d989ce..2e9e4ba25a37 100644 --- a/packages/react-native-reanimated/src/common/types/helpers.ts +++ b/packages/react-native-reanimated/src/common/types/helpers.ts @@ -14,7 +14,6 @@ export type NonMutable = T extends object ? Readonly : T; export type AnyRecord = Record; export type UnknownRecord = Record; -export type ReadonlyRecord = Readonly>; export type AnyComponent = ComponentType; diff --git a/packages/react-native-reanimated/src/css/native/registry.ts b/packages/react-native-reanimated/src/css/native/registry.ts index 49eee6f3350e..630d12b6b5e5 100644 --- a/packages/react-native-reanimated/src/css/native/registry.ts +++ b/packages/react-native-reanimated/src/css/native/registry.ts @@ -1,10 +1,8 @@ 'use strict'; -import type { PropsBuilder, StyleBuilderConfig } from '../../common'; import { BASE_PROPERTIES_CONFIG, - createStyleBuilder, ReanimatedError, - ValueProcessorTarget, + createNativePropsBuilder, } from '../../common'; export const ERROR_MESSAGES = { @@ -12,31 +10,37 @@ export const ERROR_MESSAGES = { `CSS props builder for component ${componentName} was not found`, }; -const basePropsBuilder = createStyleBuilder(BASE_PROPERTIES_CONFIG, { - separatelyInterpolatedNestedProperties: [ - 'boxShadow', - 'shadowOffset', - 'textShadowOffset', - 'transformOrigin', - ], - target: ValueProcessorTarget.CSS, -}); +type CSSPropsBuilder = ReturnType; -const PROPS_BUILDERS: Record = {}; +const DEFAULT_SEPARATELY_INTERPOLATED_NESTED_PROPERTIES = new Set([ + 'boxShadow', + 'shadowOffset', + 'textShadowOffset', + 'transformOrigin', +]); + +const COMPONENT_SEPARATELY_INTERPOLATED_NESTED_PROPERTIES = new Map< + string, + Set +>(); + +const basePropsBuilder = createNativePropsBuilder(BASE_PROPERTIES_CONFIG); + +const PROPS_BUILDERS: Record = {}; export function hasPropsBuilder(componentName: string): boolean { return !!PROPS_BUILDERS[componentName] || componentName.startsWith('RCT'); } -export function getPropsBuilder(componentName: string): PropsBuilder { +export function getPropsBuilder(componentName: string) { const propsBuilder = PROPS_BUILDERS[componentName]; if (propsBuilder) { return propsBuilder; } - // This captures all React Native components if (componentName.startsWith('RCT')) { + // This captures all React Native components (prefixed with RCT) return basePropsBuilder; } @@ -45,9 +49,26 @@ export function getPropsBuilder(componentName: string): PropsBuilder { export function registerComponentPropsBuilder( componentName: string, - config: StyleBuilderConfig + config: PropsBuilderConfig, + options: { + separatelyInterpolatedNestedProperties?: readonly string[]; + } = {} ) { - PROPS_BUILDERS[componentName] = createStyleBuilder(config, { - target: ValueProcessorTarget.CSS, - }); + PROPS_BUILDERS[componentName] = createNativePropsBuilder(config); + + if (options.separatelyInterpolatedNestedProperties?.length) { + COMPONENT_SEPARATELY_INTERPOLATED_NESTED_PROPERTIES.set( + componentName, + new Set(options.separatelyInterpolatedNestedProperties) + ); + } +} + +export function getSeparatelyInterpolatedNestedProperties( + componentName: string +): ReadonlySet { + return ( + COMPONENT_SEPARATELY_INTERPOLATED_NESTED_PROPERTIES.get(componentName) ?? + DEFAULT_SEPARATELY_INTERPOLATED_NESTED_PROPERTIES + ); } From 98f7a14684fecb9961077b2b3c811002901fab46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81opaci=C5=84ski?= Date: Wed, 3 Dec 2025 16:27:09 +0100 Subject: [PATCH 5/7] Some progress --- .../src/common/style/createPropsBuilder.ts | 125 ++++++------------ .../src/common/style/propsBuilder.ts | 51 +++---- 2 files changed, 58 insertions(+), 118 deletions(-) diff --git a/packages/react-native-reanimated/src/common/style/createPropsBuilder.ts b/packages/react-native-reanimated/src/common/style/createPropsBuilder.ts index 0a2b0c514648..fabba1921cd5 100644 --- a/packages/react-native-reanimated/src/common/style/createPropsBuilder.ts +++ b/packages/react-native-reanimated/src/common/style/createPropsBuilder.ts @@ -12,11 +12,9 @@ const MAX_PROCESS_DEPTH = 10; type CreatePropsBuilderParams = { config: TPropsConfig; - processConfigEntry: (params: { - configValue: TPropsConfig[keyof TPropsConfig]; - config: TPropsConfig; - }) => ValueProcessor | TPropsConfig[keyof TPropsConfig] | undefined; - buildProps: (props: Readonly) => UnknownRecord; + processConfigValue: ( + configValue: TPropsConfig[keyof TPropsConfig] + ) => ValueProcessor | TPropsConfig[keyof TPropsConfig] | undefined; }; type CreateStyleBuilderResult = { @@ -27,43 +25,46 @@ type CreateStyleBuilderResult = { target?: ValueProcessorTarget; } ): UnknownRecord; -} +}; -export default function createPropsBuilder({ - processConfigEntry, -config, +export default function createPropsBuilder< + TProps extends UnknownRecord, + TPropsConfig extends UnknownRecord, +>({ + processConfigValue, + config, }: CreatePropsBuilderParams): CreateStyleBuilderResult { - const processedConfig = Object.entries(config).reduce< - Record - >((acc, [key, configValue]) => { - let processedEntry: ReturnType = configValue as TPropsConfig[keyof TPropsConfig]; - - let depth = 0; - do { - if (++depth > MAX_PROCESS_DEPTH) { - throw new ReanimatedError( - `Max process depth for props builder reached for property ${key}` - ); - } - processedEntry = processConfigEntry({ - configValue: processedEntry, - config, - }); - } while (processedEntry && typeof processedEntry !== 'function'); + const processedConfig = Object.entries(config).reduce< + Record + >((acc, [key, configValue]) => { + let processedValue: ReturnType = + configValue as TPropsConfig[keyof TPropsConfig]; + + let depth = 0; + while (processedValue) { + if (++depth > MAX_PROCESS_DEPTH) { + throw new ReanimatedError( + `Max process depth for props builder reached for property ${key}` + ); + } - if (processedEntry) { - acc[key] = processedEntry as ValueProcessor; + if (typeof processedValue === 'function') { + acc[key] = processedValue as ValueProcessor; + break; } - return acc; - }, {}); + processedValue = processConfigValue(processedValue); + } + + return acc; + }, {}); - return { - build( - props: Readonly, - { - includeUndefined = false, - target = ValueProcessorTarget.Default, + return { + build( + props: Readonly, + { + includeUndefined = false, + target = ValueProcessorTarget.Default, }: { includeUndefined?: boolean; target?: ValueProcessorTarget; @@ -71,13 +72,14 @@ config, ) { 'worklet'; const context: ValueProcessorContext = { target }; - const processed = Object.entries(props).reduce( + + return Object.entries(props).reduce( (acc, [key, value]) => { const processor = processedConfig[key]; if (!processor) { // Props is not supported, skip it - return acc; + return acc; } const processedValue = processor(value, context); @@ -100,53 +102,6 @@ config, }, {} ); - - return processed; }, }; } - -// // <<<<<<< WEB STYLE BUILDER >>>>>>> - -// const isRuleBuilder =

( -// value: unknown -// ): value is RuleBuilder

=> value instanceof RuleBuilderImpl; - -// const webStyleBuilder = createStyleBuilder({ -// config: WEB_BASE_PROPERTIES_CONFIG, -// ctx: { -// ruleBuildersSet: new Set>(), -// }, -// process: ({ configValue, config, ctx }) => { -// if (configValue === true) { -// return (value) => String(value); -// } -// if (typeof configValue === 'string') { -// return (value) => -// hasSuffix(value) ? value : `${String(value)}${configValue}`; -// } -// if (isConfigPropertyAlias(configValue)) { -// return config[configValue.as]; -// } -// if (hasValueProcessor(configValue)) { -// return (value) => configValue.process(value); -// } -// if (isRuleBuilder(configValue)) { -// return (value) => { -// ctx.ruleBuildersSet.add(configValue); -// configValue.add(value); -// }; -// } -// }, -// build: (props) => { -// const entries = Object.entries(props); - -// if (entries.length === 0) { -// return null; -// } - -// return entries -// .map(([key, value]) => `${kebabizeCamelCase(key)}: ${String(value)}`) -// .join('; '); -// }, -// }); diff --git a/packages/react-native-reanimated/src/common/style/propsBuilder.ts b/packages/react-native-reanimated/src/common/style/propsBuilder.ts index 999c212e3942..28162f62b3c3 100644 --- a/packages/react-native-reanimated/src/common/style/propsBuilder.ts +++ b/packages/react-native-reanimated/src/common/style/propsBuilder.ts @@ -1,5 +1,10 @@ 'use strict'; -import { ConfigPropertyAlias, PlainStyle, UnknownRecord, type ValueProcessor } from '../types'; +import type { + ConfigPropertyAlias, + PlainStyle, + UnknownRecord, + ValueProcessor, +} from '../types'; import { isConfigPropertyAlias, isRecord } from '../utils'; import { BASE_PROPERTIES_CONFIG } from './config'; import createPropsBuilder from './createPropsBuilder'; @@ -20,21 +25,25 @@ type PropsBuilderPropertyConfig< // type than in the React Native stylesheet (e.g. number for colors instead of string) // eslint-disable-next-line @typescript-eslint/no-explicit-any process: ValueProcessor[K], any>; // for custom value processing - }; + }; export function createNativePropsBuilder( - config: Required<{ [K in keyof TProps]: PropsBuilderPropertyConfig }> + config: Required<{ + [K in keyof TProps]: PropsBuilderPropertyConfig; + }> ) { - return createPropsBuilder({ + type TConfig = typeof config; + + return createPropsBuilder({ config, - processConfigEntry: ({ configValue, config }) => { + processConfigValue(configValue) { if (configValue === true) { return (value) => { 'worklet'; return value; }; } - if (isConfigPropertyAlias(configValue)) { + if (isConfigPropertyAlias(configValue)) { return config[configValue.as]; } if (hasValueProcessor(configValue)) { @@ -47,32 +56,8 @@ export function createNativePropsBuilder( }); } - - -// = createPropsBuilder({ -// processConfigEntry: ({ configValue, config }) => { -// if (configValue === true) { -// return (value) => { -// 'worklet'; -// return value; -// }; -// } -// if (isConfigPropertyAlias(configValue)) { -// return config[configValue.as]; -// } -// if (hasValueProcessor(configValue)) { -// return (value, context) => { -// 'worklet'; -// return configValue.process(value, context); -// }; -// } -// }, -// buildProps: (props) => { -// 'worklet'; -// return props; -// }, -// }); - -const propsBuilder = createNativePropsBuilder(BASE_PROPERTIES_CONFIG); +const propsBuilder = createNativePropsBuilder( + BASE_PROPERTIES_CONFIG +); export default propsBuilder; From aa5d623295f1d3fa2bf718ab5f56fae862287efe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81opaci=C5=84ski?= Date: Wed, 3 Dec 2025 21:16:36 +0000 Subject: [PATCH 6/7] Much progress on the props builder adjustments --- .../__tests__/createPropsBuilder.test.ts | 172 ++++++++++++++++++ .../__tests__/createStyleBuilder.test.ts | 72 -------- .../src/common/style/createPropsBuilder.ts | 4 +- .../src/common/style/index.ts | 1 + .../src/common/style/processors/shadows.ts | 4 +- .../src/common/style/propsBuilder.ts | 6 + .../src/css/native/__tests__/registry.test.ts | 4 +- .../native/keyframes/CSSKeyframesRuleImpl.ts | 3 +- .../src/css/native/managers/CSSManager.ts | 13 +- .../animation/__tests__/animationName.test.ts | 29 ++- .../animation/__tests__/keyframes.test.ts | 58 +++--- .../normalization/animation/keyframes.ts | 38 ++-- .../src/css/native/registry.ts | 5 +- .../src/css/svg/native/configs/common.ts | 22 +-- 14 files changed, 277 insertions(+), 154 deletions(-) create mode 100644 packages/react-native-reanimated/src/common/style/__tests__/createPropsBuilder.test.ts delete mode 100644 packages/react-native-reanimated/src/common/style/__tests__/createStyleBuilder.test.ts diff --git a/packages/react-native-reanimated/src/common/style/__tests__/createPropsBuilder.test.ts b/packages/react-native-reanimated/src/common/style/__tests__/createPropsBuilder.test.ts new file mode 100644 index 000000000000..1901a4ade934 --- /dev/null +++ b/packages/react-native-reanimated/src/common/style/__tests__/createPropsBuilder.test.ts @@ -0,0 +1,172 @@ +'use strict'; + +import type { ValueProcessor } from '../../types'; +import { ValueProcessorTarget } from '../../types'; +import { ReanimatedError } from '../../errors'; +import createPropsBuilder from '../createPropsBuilder'; + +type TestStyle = { + width?: number; + margin?: string | number; + borderRadius?: number; + padding?: number; + shadowColor?: number; + shadowOpacity?: number; + shadowRadius?: number; + height?: number; +}; + +type ConfigEntry = boolean | { process: ValueProcessor } | 'loop'; + +type TestConfig = Record; + +const BASE_CONFIG: TestConfig = { + width: false, + margin: false, + borderRadius: false, + padding: false, + shadowColor: false, + shadowOpacity: false, + shadowRadius: false, + height: false, +}; + +const createBuilder = (configOverrides: Partial) => { + const config: TestConfig = { ...BASE_CONFIG, ...configOverrides }; + + return createPropsBuilder({ + config, + processConfigValue(configValue) { + if (configValue === true) { + return (value: unknown) => value; + } + + if (configValue === 'loop') { + return configValue; + } + + if ( + configValue && + typeof configValue === 'object' && + 'process' in configValue && + typeof configValue.process === 'function' + ) { + return configValue.process; + } + + return undefined; + }, + }); +}; + +describe(createPropsBuilder, () => { + test('skips undefined values unless includeUndefined is true', () => { + const builder = createBuilder({ + width: true, + margin: true, + borderRadius: true, + }); + + const style: TestStyle = { + width: undefined, + margin: 'auto', + borderRadius: 10, + }; + + expect(builder.build(style)).toEqual({ + margin: 'auto', + borderRadius: 10, + }); + + expect( + builder.build(style, { includeUndefined: true }) + ).toEqual({ + width: undefined, + margin: 'auto', + borderRadius: 10, + }); + }); + + test('ignores properties not present in config', () => { + const builder = createBuilder({ width: true }); + + const style: TestStyle = { + width: 120, + height: 300, + }; + + expect(builder.build(style)).toEqual({ width: 120 }); + }); + + test('passes provided context to processors', () => { + const processor = jest.fn().mockReturnValue(24); + const builder = createBuilder({ + borderRadius: { process: processor }, + }); + + builder.build({ borderRadius: 12 }, { + target: ValueProcessorTarget.CSS, + }); + + expect(processor).toHaveBeenCalledWith(12, { + target: ValueProcessorTarget.CSS, + }); + }); + + test('uses default target context when target not set', () => { + const processor = jest.fn().mockReturnValue(10); + const builder = createBuilder({ + padding: { process: processor }, + }); + + builder.build({ padding: 5 }); + + expect(processor).toHaveBeenCalledWith(5, { + target: ValueProcessorTarget.Default, + }); + }); + + test('merges record results without overwriting original props', () => { + const builder = createBuilder({ + shadowColor: { + process: () => ({ + shadowOpacity: 0.5, + shadowRadius: 6, + }), + }, + shadowOpacity: true, + shadowRadius: true, + }); + + const style: TestStyle = { + shadowColor: 0xff0000, + shadowOpacity: 0.8, + }; + + expect(builder.build(style)).toEqual({ + shadowOpacity: 0.8, + shadowRadius: 6, + }); + }); + + test('allows processors to return undefined based on includeUndefined option', () => { + const builder = createBuilder({ + width: { + process: () => undefined, + }, + }); + + expect(builder.build({ width: 10 })).toEqual({}); + expect( + builder.build({ width: 10 }, { includeUndefined: true }) + ).toEqual({ width: undefined }); + }); + + test('throws when processor resolution exceeds maximum depth', () => { + expect(() => + createBuilder({ + width: 'loop', + }) + ).toThrow(new ReanimatedError('Max process depth for props builder reached for property width')); + }); +}); diff --git a/packages/react-native-reanimated/src/common/style/__tests__/createStyleBuilder.test.ts b/packages/react-native-reanimated/src/common/style/__tests__/createStyleBuilder.test.ts deleted file mode 100644 index fb98c3376088..000000000000 --- a/packages/react-native-reanimated/src/common/style/__tests__/createStyleBuilder.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -'use strict'; -import type { PlainStyle } from '../../types'; -import { ValueProcessorTarget } from '../../types'; -import createStyleBuilder from '../createStyleBuilder-old'; -// TODO - add more tests - -describe(createStyleBuilder, () => { - const styleBuilder = createStyleBuilder({ - width: true, - margin: true, - borderRadius: true, - flexDirection: true, - }); - - test("doesn't include undefined values", () => { - const style: PlainStyle = { - width: undefined, - margin: 'auto', - borderRadius: 10, - flexDirection: undefined, - }; - - expect(styleBuilder.buildFrom(style)).toEqual({ - margin: 'auto', - borderRadius: 10, - }); - }); - - test("doesn't include properties that are not in the config", () => { - const style: PlainStyle = { - width: 100, - height: 100, // height is not in the config - }; - - expect(styleBuilder.buildFrom(style)).toEqual({ - width: 100, - }); - }); - - test('passes context to processors', () => { - const processor = jest.fn(); - - const builder = createStyleBuilder( - { - borderRadius: { - process: processor, - }, - }, - { - target: ValueProcessorTarget.CSS, - } - ); - - builder.buildFrom({ borderRadius: 5 }); - - expect(processor).toHaveBeenCalledWith(5, { - target: ValueProcessorTarget.CSS, - }); - }); - - test('uses default target when none provided', () => { - const processor = jest.fn(); - - const builder = createStyleBuilder({ padding: { process: processor } }); - - builder.buildFrom({ padding: 8 }); - - expect(processor).toHaveBeenCalledWith(8, { - target: ValueProcessorTarget.Default, - }); - }); -}); diff --git a/packages/react-native-reanimated/src/common/style/createPropsBuilder.ts b/packages/react-native-reanimated/src/common/style/createPropsBuilder.ts index fabba1921cd5..d8788a90e569 100644 --- a/packages/react-native-reanimated/src/common/style/createPropsBuilder.ts +++ b/packages/react-native-reanimated/src/common/style/createPropsBuilder.ts @@ -17,7 +17,7 @@ type CreatePropsBuilderParams = { ) => ValueProcessor | TPropsConfig[keyof TPropsConfig] | undefined; }; -type CreateStyleBuilderResult = { +export type PropsBuilderResult = { build( props: TProps, options?: { @@ -33,7 +33,7 @@ export default function createPropsBuilder< >({ processConfigValue, config, -}: CreatePropsBuilderParams): CreateStyleBuilderResult { +}: CreatePropsBuilderParams): PropsBuilderResult { const processedConfig = Object.entries(config).reduce< Record >((acc, [key, configValue]) => { diff --git a/packages/react-native-reanimated/src/common/style/index.ts b/packages/react-native-reanimated/src/common/style/index.ts index 6fe1f5837858..b197a528eb92 100644 --- a/packages/react-native-reanimated/src/common/style/index.ts +++ b/packages/react-native-reanimated/src/common/style/index.ts @@ -3,3 +3,4 @@ export * from './config'; export * from './processors'; export { default as createPropsBuilder } from './createPropsBuilder'; export { default as propsBuilder, createNativePropsBuilder } from './propsBuilder'; +export type * from './types'; diff --git a/packages/react-native-reanimated/src/common/style/processors/shadows.ts b/packages/react-native-reanimated/src/common/style/processors/shadows.ts index 08886b5e981c..9bc527090efd 100644 --- a/packages/react-native-reanimated/src/common/style/processors/shadows.ts +++ b/packages/react-native-reanimated/src/common/style/processors/shadows.ts @@ -35,10 +35,10 @@ const parseBlurRadius = (value: string) => { export const processBoxShadowNative: ValueProcessor< ReadonlyArray | string, - ProcessedBoxShadowValue[] + ProcessedBoxShadowValue[] | null > = (value, context) => { if (value === 'none') { - return; + return null; } const parsedShadow = diff --git a/packages/react-native-reanimated/src/common/style/propsBuilder.ts b/packages/react-native-reanimated/src/common/style/propsBuilder.ts index 28162f62b3c3..e8ee9dcfb462 100644 --- a/packages/react-native-reanimated/src/common/style/propsBuilder.ts +++ b/packages/react-native-reanimated/src/common/style/propsBuilder.ts @@ -27,6 +27,12 @@ type PropsBuilderPropertyConfig< process: ValueProcessor[K], any>; // for custom value processing }; +export type NativePropsBuilder = ReturnType< + typeof createPropsBuilder; + }>> +>; + export function createNativePropsBuilder( config: Required<{ [K in keyof TProps]: PropsBuilderPropertyConfig; diff --git a/packages/react-native-reanimated/src/css/native/__tests__/registry.test.ts b/packages/react-native-reanimated/src/css/native/__tests__/registry.test.ts index e034196a4cee..1bd99e4de942 100644 --- a/packages/react-native-reanimated/src/css/native/__tests__/registry.test.ts +++ b/packages/react-native-reanimated/src/css/native/__tests__/registry.test.ts @@ -37,14 +37,14 @@ describe('registry', () => { const propsBuilder = getPropsBuilder(componentName); expect(propsBuilder).toBeDefined(); - expect(typeof propsBuilder.buildFrom).toBe('function'); + expect(typeof propsBuilder.build).toBe('function'); }); test('returns base props builder for RCT prefixed components', () => { const propsBuilder = getPropsBuilder('RCTView'); expect(propsBuilder).toBeDefined(); - expect(typeof propsBuilder.buildFrom).toBe('function'); + expect(typeof propsBuilder.build).toBe('function'); }); test('throws error for unregistered component names', () => { diff --git a/packages/react-native-reanimated/src/css/native/keyframes/CSSKeyframesRuleImpl.ts b/packages/react-native-reanimated/src/css/native/keyframes/CSSKeyframesRuleImpl.ts index c4cb7be2eb88..ca64dd3a3820 100644 --- a/packages/react-native-reanimated/src/css/native/keyframes/CSSKeyframesRuleImpl.ts +++ b/packages/react-native-reanimated/src/css/native/keyframes/CSSKeyframesRuleImpl.ts @@ -3,7 +3,6 @@ import type { PlainStyle } from '../../../common'; import { CSSKeyframesRuleBase } from '../../models'; import type { CSSAnimationKeyframes } from '../../types'; import { normalizeAnimationKeyframes } from '../normalization'; -import { getPropsBuilder } from '../registry'; import type { NormalizedCSSAnimationKeyframesConfig } from '../types'; export default class CSSKeyframesRuleImpl< @@ -24,7 +23,7 @@ export default class CSSKeyframesRuleImpl< if (!this.normalizedKeyframesCache_[viewName]) { this.normalizedKeyframesCache_[viewName] = normalizeAnimationKeyframes( this.cssRules, - getPropsBuilder(viewName) + viewName ); } diff --git a/packages/react-native-reanimated/src/css/native/managers/CSSManager.ts b/packages/react-native-reanimated/src/css/native/managers/CSSManager.ts index 831e31892af7..66a68c179e73 100644 --- a/packages/react-native-reanimated/src/css/native/managers/CSSManager.ts +++ b/packages/react-native-reanimated/src/css/native/managers/CSSManager.ts @@ -1,7 +1,5 @@ 'use strict'; -import type { AnyRecord } from '../../../common'; import { ReanimatedError } from '../../../common'; -import type { PropsBuilder } from '../../../common/style'; import type { ShadowNodeWrapper } from '../../../commonTypes'; import type { ViewInfo } from '../../../createAnimatedComponent/commonTypes'; import type { CSSStyle } from '../../types'; @@ -17,7 +15,9 @@ export default class CSSManager implements ICSSManager { private readonly cssTransitionsManager: CSSTransitionsManager; private readonly viewTag: number; private readonly viewName: string; - private readonly propsBuilder: PropsBuilder | null = null; + private readonly propsBuilder: + | ReturnType + | null = null; private isFirstUpdate: boolean = true; constructor({ shadowNodeWrapper, viewTag, viewName = 'RCTView' }: ViewInfo) { @@ -46,7 +46,12 @@ export default class CSSManager implements ICSSManager { ); } - const normalizedStyle = this.propsBuilder?.buildFrom(filteredStyle); + let normalizedStyle: CSSStyle | null = null; + if (this.propsBuilder) { + normalizedStyle = this.propsBuilder.build( + filteredStyle as Parameters[0] + ) as CSSStyle | null; + } // If the update is called during the first css style update, we won't // trigger CSS transitions and set styles before attaching CSS transitions diff --git a/packages/react-native-reanimated/src/css/native/normalization/animation/__tests__/animationName.test.ts b/packages/react-native-reanimated/src/css/native/normalization/animation/__tests__/animationName.test.ts index 47ca343b7686..c7e7ac7392b1 100644 --- a/packages/react-native-reanimated/src/css/native/normalization/animation/__tests__/animationName.test.ts +++ b/packages/react-native-reanimated/src/css/native/normalization/animation/__tests__/animationName.test.ts @@ -1,12 +1,9 @@ 'use strict'; import { ReanimatedError } from '../../../../../common'; import type { CSSAnimationKeyframeSelector } from '../../../../types'; -import { getPropsBuilder } from '../../../registry'; import { ERROR_MESSAGES, normalizeAnimationKeyframes } from '../keyframes'; describe(normalizeAnimationKeyframes, () => { - const propsBuilder = getPropsBuilder('RCTView'); // Must be a valid view name - describe('offset normalization', () => { describe('when offset is valid', () => { test.each([ @@ -22,7 +19,7 @@ describe(normalizeAnimationKeyframes, () => { expect( normalizeAnimationKeyframes( { [offset]: { opacity: 1 } }, - propsBuilder + 'RCTView' ) ).toEqual({ keyframesStyle: { opacity: [{ offset: expected, value: 1 }] }, @@ -39,7 +36,7 @@ describe(normalizeAnimationKeyframes, () => { expect(() => normalizeAnimationKeyframes( { [value]: { opacity: 1 } }, - propsBuilder + 'RCTView' ) ).toThrow( new ReanimatedError(ERROR_MESSAGES.invalidOffsetType(value)) @@ -56,7 +53,7 @@ describe(normalizeAnimationKeyframes, () => { expect(() => normalizeAnimationKeyframes( { [value]: { opacity: 1 } }, - propsBuilder + 'RCTView' ) ).toThrow( new ReanimatedError(ERROR_MESSAGES.invalidOffsetRange(value)) @@ -78,7 +75,7 @@ describe(normalizeAnimationKeyframes, () => { expect( normalizeAnimationKeyframes( { [offset]: { opacity: 1 } }, - propsBuilder + 'RCTView' ) ).toEqual({ keyframeTimingFunctions: {}, @@ -100,7 +97,7 @@ describe(normalizeAnimationKeyframes, () => { ])('throws an error for %p', (offset, errorMsg) => { const value = offset as CSSAnimationKeyframeSelector; expect(() => - normalizeAnimationKeyframes({ [value]: { opacity: 1 } }, propsBuilder) + normalizeAnimationKeyframes({ [value]: { opacity: 1 } }, 'RCTView') ).toThrow(new ReanimatedError(errorMsg)); }); }); @@ -115,7 +112,7 @@ describe(normalizeAnimationKeyframes, () => { '50%': { opacity: 0.5 }, to: { opacity: 1 }, }, - propsBuilder + 'RCTView' ) ).toEqual({ keyframesStyle: { @@ -136,7 +133,7 @@ describe(normalizeAnimationKeyframes, () => { from: { shadowOffset: { width: 0, height: 0 } }, to: { shadowOffset: { width: 10, height: 10 } }, }, - propsBuilder + 'RCTView' ) ).toEqual({ keyframesStyle: { @@ -165,7 +162,7 @@ describe(normalizeAnimationKeyframes, () => { '25%': { opacity: 0.25 }, from: { opacity: 0 }, }, - propsBuilder + 'RCTView' ) ).toEqual({ keyframesStyle: { @@ -188,7 +185,7 @@ describe(normalizeAnimationKeyframes, () => { from: { transform: [{ scale: 0 }, { rotate: '0deg' }] }, to: { transform: [{ scale: 1 }, { rotate: '360deg' }] }, }, - propsBuilder + 'RCTView' ) ).toEqual({ keyframesStyle: { @@ -208,7 +205,7 @@ describe(normalizeAnimationKeyframes, () => { from: { opacity: 0, transform: undefined }, to: { opacity: 1 }, }, - propsBuilder + 'RCTView' ) ).toEqual({ keyframesStyle: { @@ -229,7 +226,7 @@ describe(normalizeAnimationKeyframes, () => { '50%': { opacity: 0.5 }, to: {}, }, - propsBuilder + 'RCTView' ) ).toEqual({ keyframesStyle: { @@ -250,7 +247,7 @@ describe(normalizeAnimationKeyframes, () => { '50%': { opacity: 0.75 }, '75%': { opacity: 1, animationTimingFunction: 'ease-out' }, }, - propsBuilder + 'RCTView' ) ).toEqual({ keyframesStyle: { @@ -281,7 +278,7 @@ describe(normalizeAnimationKeyframes, () => { { '0%, 100%': { opacity: 0, animationTimingFunction: 'ease-in' }, }, - propsBuilder + 'RCTView' ) ).toEqual({ keyframesStyle: { diff --git a/packages/react-native-reanimated/src/css/native/normalization/animation/__tests__/keyframes.test.ts b/packages/react-native-reanimated/src/css/native/normalization/animation/__tests__/keyframes.test.ts index c878296c2398..b001e4e7671f 100644 --- a/packages/react-native-reanimated/src/css/native/normalization/animation/__tests__/keyframes.test.ts +++ b/packages/react-native-reanimated/src/css/native/normalization/animation/__tests__/keyframes.test.ts @@ -9,6 +9,11 @@ import { processKeyframes, } from '../keyframes'; +type PropsBuilderInstance = ReturnType; +type BuildFn = PropsBuilderInstance['build']; +type BuildReturn = ReturnType; +type BuildArgs = Parameters; + describe(normalizeKeyframeSelector, () => { describe('single selector', () => { describe('keyword', () => { @@ -82,28 +87,21 @@ describe(normalizeKeyframeSelector, () => { }); }); -function mockPropsBuilder( - separatelyInterpolatedNestedProperties: string[] = [] -) { - const separatelyInterpolatedNestedPropertiesSet = new Set( - separatelyInterpolatedNestedProperties +const createMockPropsBuilder = () => { + const buildMock = jest.fn((style) => + style as BuildReturn ); return { - buildFrom: jest.fn().mockImplementation((props) => props), - isSeparatelyInterpolatedNestedProperty: jest - .fn() - .mockImplementation((property) => - separatelyInterpolatedNestedPropertiesSet.has(property) - ), - add: jest.fn(), + builder: { build: buildMock } as PropsBuilderInstance, + buildMock, }; -} +}; describe(processKeyframes, () => { describe('offset handling', () => { test('sorts keyframes and accepts percentages', () => { - const propsBuilder = mockPropsBuilder(); + const { builder } = createMockPropsBuilder(); const keyframes = { '75%': { opacity: 0.75 }, from: { opacity: 0 }, @@ -111,7 +109,7 @@ describe(processKeyframes, () => { to: { opacity: 1 }, }; - expect(processKeyframes(keyframes, propsBuilder)).toEqual([ + expect(processKeyframes(keyframes, builder)).toEqual([ { offset: 0, style: { opacity: 0 } }, { offset: 0.25, style: { opacity: 0.25 } }, { offset: 0.75, style: { opacity: 0.75 } }, @@ -120,13 +118,13 @@ describe(processKeyframes, () => { }); test('splits multi-selector entries into separate keyframes', () => { - const propsBuilder = mockPropsBuilder(); + const { builder } = createMockPropsBuilder(); const keyframes = { 'from, 50%': { opacity: 0.5 }, to: { opacity: 1 }, }; - expect(processKeyframes(keyframes, propsBuilder)).toEqual([ + expect(processKeyframes(keyframes, builder)).toEqual([ { offset: 0, style: { opacity: 0.5 } }, { offset: 0.5, style: { opacity: 0.5 } }, { offset: 1, style: { opacity: 1 } }, @@ -141,7 +139,9 @@ describe(processKeyframes, () => { '100%': { transform: [{ translateX: 100 }] }, }; - expect(processKeyframes(keyframes, getPropsBuilder('RCTView'))).toEqual([ + expect( + processKeyframes(keyframes, getPropsBuilder('RCTView')) + ).toEqual([ { offset: 0, style: { transform: [{ translateX: 0 }] } }, { offset: 1, style: { transform: [{ translateX: 100 }] } }, ]); @@ -280,30 +280,34 @@ describe(processKeyframes, () => { }); test('drops keyframes when processed style is undefined', () => { - const propsBuilder = mockPropsBuilder(['shadowOffset']); - propsBuilder.buildFrom + const { builder, buildMock } = createMockPropsBuilder(); + buildMock .mockImplementationOnce(() => undefined) - .mockImplementation((style) => style); + .mockImplementation((style) => style as BuildReturn); const keyframes = { from: { shadowOffset: { width: 0, height: 0 } }, to: { shadowOffset: { width: 10, height: 5 } }, }; - expect(processKeyframes(keyframes, propsBuilder)).toEqual([ + expect(processKeyframes(keyframes, builder)).toEqual([ { offset: 1, style: { shadowOffset: { width: 10, height: 5 } } }, ]); + + buildMock.mockRestore(); }); test('merges styles for duplicate offsets', () => { - const propsBuilder = mockPropsBuilder(); + const { builder } = createMockPropsBuilder(); const keyframes = { '0%': { opacity: 0.5 }, '0': { transform: [{ scale: 1 }] }, '100%': { opacity: 1 }, }; - expect(processKeyframes(keyframes, propsBuilder)).toEqual([ + expect( + processKeyframes(keyframes, builder) + ).toEqual([ { offset: 0, style: { opacity: 0.5, transform: [{ scale: 1 }] }, @@ -314,8 +318,6 @@ describe(processKeyframes, () => { }); describe(normalizeAnimationKeyframes, () => { - const propsBuilder = getPropsBuilder('RCTView'); - test('aggregates styles and timing functions across keyframes', () => { const result = normalizeAnimationKeyframes( { @@ -326,7 +328,7 @@ describe(normalizeAnimationKeyframes, () => { }, to: { opacity: 1 }, }, - propsBuilder + 'RCTView' ); expect(result).toEqual({ @@ -353,7 +355,7 @@ describe(normalizeAnimationKeyframes, () => { from: { opacity: 0, animationTimingFunction: 'ease-in' }, to: { opacity: 1, animationTimingFunction: 'ease-out' }, }, - propsBuilder + 'RCTView' ); expect(result).toEqual({ diff --git a/packages/react-native-reanimated/src/css/native/normalization/animation/keyframes.ts b/packages/react-native-reanimated/src/css/native/normalization/animation/keyframes.ts index af9566dfd116..1441d0d4b18a 100644 --- a/packages/react-native-reanimated/src/css/native/normalization/animation/keyframes.ts +++ b/packages/react-native-reanimated/src/css/native/normalization/animation/keyframes.ts @@ -1,5 +1,5 @@ 'use strict'; -import type { AnyRecord, PropsBuilder } from '../../../../common'; +import type { AnyRecord } from '../../../../common'; import { isDefined, isNumber, ReanimatedError } from '../../../../common'; import type { StyleProps } from '../../../../commonTypes'; import { PERCENTAGE_REGEX } from '../../../constants'; @@ -14,6 +14,10 @@ import type { NormalizedCSSKeyframeTimingFunctions, } from '../../types'; import { normalizeTimingFunction } from '../common'; +import { + getPropsBuilder, + getSeparatelyInterpolatedNestedProperties, +} from '../../registry'; export const ERROR_MESSAGES = { invalidOffsetType: (selector: CSSAnimationKeyframeSelector) => @@ -67,12 +71,14 @@ type ProcessedKeyframes = Array<{ export function processKeyframes( keyframes: CSSAnimationKeyframes, - styleBuilder: PropsBuilder + propsBuilder: ReturnType ): ProcessedKeyframes { return Object.entries(keyframes) .flatMap( ([selector, { animationTimingFunction = undefined, ...style } = {}]) => { - const normalizedStyle = styleBuilder.buildFrom(style); + const normalizedStyle = propsBuilder.build( + style as Parameters['build']>[0] + ); if (!normalizedStyle) { return []; } @@ -98,19 +104,19 @@ export function processKeyframes( }, []); } -function processStyleProperties( +function processStyleProperties( offset: number, - style: S, + style: AnyRecord, keyframeStyle: AnyRecord, - styleBuilder: PropsBuilder + separatelyInterpolatedNestedProperties: ReadonlySet ) { Object.entries(style).forEach(([property, value]) => { if (!isDefined(value)) { return; } - if (typeof value === 'object') { - if (styleBuilder.isSeparatelyInterpolatedNestedProperty(property)) { + if (value && typeof value === 'object' && !Array.isArray(value)) { + if (separatelyInterpolatedNestedProperties.has(property)) { if (!keyframeStyle[property]) { keyframeStyle[property] = Array.isArray(value) ? [] : {}; } @@ -118,7 +124,7 @@ function processStyleProperties( offset, value, keyframeStyle[property], - styleBuilder + separatelyInterpolatedNestedProperties ); return; } @@ -133,14 +139,22 @@ function processStyleProperties( export function normalizeAnimationKeyframes( keyframes: CSSAnimationKeyframes, - styleBuilder: PropsBuilder + viewName: string ): NormalizedCSSAnimationKeyframesConfig { + const propsBuilder = getPropsBuilder(viewName); + const separatelyInterpolatedNestedProperties = + getSeparatelyInterpolatedNestedProperties(viewName); const keyframesStyle: NormalizedCSSKeyframesStyle = {}; const timingFunctions: NormalizedCSSKeyframeTimingFunctions = {}; - processKeyframes(keyframes, styleBuilder).forEach( + processKeyframes(keyframes, propsBuilder).forEach( ({ offset, style, timingFunction }) => { - processStyleProperties(offset, style, keyframesStyle, styleBuilder); + processStyleProperties( + offset, + style, + keyframesStyle, + separatelyInterpolatedNestedProperties + ); if (timingFunction && offset < 1) { timingFunctions[offset] = normalizeTimingFunction(timingFunction); } diff --git a/packages/react-native-reanimated/src/css/native/registry.ts b/packages/react-native-reanimated/src/css/native/registry.ts index 630d12b6b5e5..3bc4066a9084 100644 --- a/packages/react-native-reanimated/src/css/native/registry.ts +++ b/packages/react-native-reanimated/src/css/native/registry.ts @@ -4,14 +4,13 @@ import { ReanimatedError, createNativePropsBuilder, } from '../../common'; +import type { PropsBuilderConfig } from '../../common'; export const ERROR_MESSAGES = { propsBuilderNotFound: (componentName: string) => `CSS props builder for component ${componentName} was not found`, }; -type CSSPropsBuilder = ReturnType; - const DEFAULT_SEPARATELY_INTERPOLATED_NESTED_PROPERTIES = new Set([ 'boxShadow', 'shadowOffset', @@ -26,7 +25,7 @@ const COMPONENT_SEPARATELY_INTERPOLATED_NESTED_PROPERTIES = new Map< const basePropsBuilder = createNativePropsBuilder(BASE_PROPERTIES_CONFIG); -const PROPS_BUILDERS: Record = {}; +const PROPS_BUILDERS: Record> = {}; export function hasPropsBuilder(componentName: string): boolean { return !!PROPS_BUILDERS[componentName] || componentName.startsWith('RCT'); diff --git a/packages/react-native-reanimated/src/css/svg/native/configs/common.ts b/packages/react-native-reanimated/src/css/svg/native/configs/common.ts index 4e57edaa60c8..d9c5dc3311f2 100644 --- a/packages/react-native-reanimated/src/css/svg/native/configs/common.ts +++ b/packages/react-native-reanimated/src/css/svg/native/configs/common.ts @@ -16,7 +16,7 @@ import type { TransformProps, } from 'react-native-svg'; -import type { StyleBuilderConfig } from '../../../../common'; +import type { PropsBuilderConfig } from '../../../../common'; import { convertStringToNumber, processColorSVG, @@ -26,11 +26,11 @@ import { const colorAttributes = { process: processColorSVG }; -const colorProps: StyleBuilderConfig = { +const colorProps: PropsBuilderConfig = { color: colorAttributes, }; -const fillProps: StyleBuilderConfig = { +const fillProps: PropsBuilderConfig = { fill: colorAttributes, fillOpacity: { process: processOpacity }, fillRule: { @@ -41,7 +41,7 @@ const fillProps: StyleBuilderConfig = { }, }; -const strokeProps: StyleBuilderConfig = { +const strokeProps: PropsBuilderConfig = { stroke: colorAttributes, strokeWidth: true, strokeOpacity: { process: processOpacity }, @@ -74,12 +74,12 @@ const strokeProps: StyleBuilderConfig = { }, }; -const clipProps: StyleBuilderConfig = { +const clipProps: PropsBuilderConfig = { clipRule: true, clipPath: true, // TODO - maybe preprocess this? }; -const transformProps: StyleBuilderConfig = { +const transformProps: PropsBuilderConfig = { translate: true, // TODO - add preprocessor (NumberArray) and split to translateX and translateY translateX: true, translateY: true, @@ -98,25 +98,25 @@ const transformProps: StyleBuilderConfig = { transform: true, // TODO - add preprocessor }; -const responderProps: StyleBuilderConfig< +const responderProps: PropsBuilderConfig< Omit > = { pointerEvents: true, }; // TODO - check what these props are doing and if we need to preprocess them -const commonMarkerProps: StyleBuilderConfig = { +const commonMarkerProps: PropsBuilderConfig = { marker: true, markerStart: true, markerMid: true, markerEnd: true, }; -const commonMaskProps: StyleBuilderConfig = { +const commonMaskProps: PropsBuilderConfig = { mask: true, // TODO - add preprocessor }; -const commonFilterProps: StyleBuilderConfig = { +const commonFilterProps: PropsBuilderConfig = { filter: true, // TODO - add preprocessor }; @@ -127,7 +127,7 @@ type NonAnimatablePropNames = | keyof NativeProps | keyof AccessibilityProps; -export type SvgStyleBuilderConfig = StyleBuilderConfig< +export type SvgStyleBuilderConfig = PropsBuilderConfig< Omit >; From 7dccdae14f3b20be02850aed9c763fd8c5b996ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81opaci=C5=84ski?= Date: Wed, 3 Dec 2025 22:16:20 +0000 Subject: [PATCH 7/7] Some progress --- .../src/common/style/createPropsBuilder.ts | 6 ++++- .../src/common/style/index.ts | 1 + .../{css/native => common/style}/registry.ts | 23 ++++++++++--------- .../src/common/style/types.ts | 17 ++++++++++---- .../src/css/native/__tests__/registry.test.ts | 2 +- .../src/css/native/index.ts | 1 - .../src/css/native/managers/CSSManager.ts | 2 +- .../animation/__tests__/keyframes.test.ts | 2 +- .../normalization/animation/keyframes.ts | 2 +- .../src/css/svg/init.ts | 2 +- .../src/css/svg/native/configs/common.ts | 2 +- 11 files changed, 37 insertions(+), 23 deletions(-) rename packages/react-native-reanimated/src/{css/native => common/style}/registry.ts (77%) diff --git a/packages/react-native-reanimated/src/common/style/createPropsBuilder.ts b/packages/react-native-reanimated/src/common/style/createPropsBuilder.ts index d8788a90e569..2edd0867b14a 100644 --- a/packages/react-native-reanimated/src/common/style/createPropsBuilder.ts +++ b/packages/react-native-reanimated/src/common/style/createPropsBuilder.ts @@ -83,12 +83,16 @@ export default function createPropsBuilder< } const processedValue = processor(value, context); + + const valueIsRecord = isRecord(value); + const processedValueIsRecord = isRecord(processedValue); + if (processedValue === undefined && !includeUndefined) { // Skip if value is undefined and we don't want to include undefined values return acc; } - if (isRecord(processedValue)) { + if (processedValueIsRecord && !valueIsRecord) { for (const processedKey in processedValue) { if (!(processedKey in props)) { acc[processedKey] = processedValue[processedKey]; diff --git a/packages/react-native-reanimated/src/common/style/index.ts b/packages/react-native-reanimated/src/common/style/index.ts index b197a528eb92..4428e32d1898 100644 --- a/packages/react-native-reanimated/src/common/style/index.ts +++ b/packages/react-native-reanimated/src/common/style/index.ts @@ -1,6 +1,7 @@ 'use strict'; export * from './config'; export * from './processors'; +export * from './registry'; export { default as createPropsBuilder } from './createPropsBuilder'; export { default as propsBuilder, createNativePropsBuilder } from './propsBuilder'; export type * from './types'; diff --git a/packages/react-native-reanimated/src/css/native/registry.ts b/packages/react-native-reanimated/src/common/style/registry.ts similarity index 77% rename from packages/react-native-reanimated/src/css/native/registry.ts rename to packages/react-native-reanimated/src/common/style/registry.ts index 3bc4066a9084..fe2e8873c786 100644 --- a/packages/react-native-reanimated/src/css/native/registry.ts +++ b/packages/react-native-reanimated/src/common/style/registry.ts @@ -1,10 +1,11 @@ 'use strict'; -import { - BASE_PROPERTIES_CONFIG, - ReanimatedError, +import { ReanimatedError } from '../errors'; +import type { PlainStyle, UnknownRecord } from '../types'; +import propsBuilder, { createNativePropsBuilder, -} from '../../common'; -import type { PropsBuilderConfig } from '../../common'; + type NativePropsBuilder, + type PropsBuilderConfig, +} from './propsBuilder'; export const ERROR_MESSAGES = { propsBuilderNotFound: (componentName: string) => @@ -23,19 +24,19 @@ const COMPONENT_SEPARATELY_INTERPOLATED_NESTED_PROPERTIES = new Map< Set >(); -const basePropsBuilder = createNativePropsBuilder(BASE_PROPERTIES_CONFIG); +const basePropsBuilder = propsBuilder as NativePropsBuilder; -const PROPS_BUILDERS: Record> = {}; +const PROPS_BUILDERS: Record = {}; export function hasPropsBuilder(componentName: string): boolean { return !!PROPS_BUILDERS[componentName] || componentName.startsWith('RCT'); } export function getPropsBuilder(componentName: string) { - const propsBuilder = PROPS_BUILDERS[componentName]; + const componentPropsBuilder = PROPS_BUILDERS[componentName]; - if (propsBuilder) { - return propsBuilder; + if (componentPropsBuilder) { + return componentPropsBuilder; } if (componentName.startsWith('RCT')) { @@ -53,7 +54,7 @@ export function registerComponentPropsBuilder( separatelyInterpolatedNestedProperties?: readonly string[]; } = {} ) { - PROPS_BUILDERS[componentName] = createNativePropsBuilder(config); + PROPS_BUILDERS[componentName] = createNativePropsBuilder(config) as NativePropsBuilder; if (options.separatelyInterpolatedNestedProperties?.length) { COMPONENT_SEPARATELY_INTERPOLATED_NESTED_PROPERTIES.set( diff --git a/packages/react-native-reanimated/src/common/style/types.ts b/packages/react-native-reanimated/src/common/style/types.ts index 3a00066e560c..1554d2c9f0cd 100644 --- a/packages/react-native-reanimated/src/common/style/types.ts +++ b/packages/react-native-reanimated/src/common/style/types.ts @@ -1,12 +1,21 @@ 'use strict'; -import type { AnyRecord, ConfigPropertyAlias, ValueProcessor } from '../types'; +import type { + AnyRecord, + ConfigPropertyAlias, + ValueProcessor, + ValueProcessorContext, +} from '../types'; export type PropsBuildMiddleware

= (props: P) => P; export type PropsBuilder

= { - isSeparatelyInterpolatedNestedProperty(property: keyof P): boolean; - add(property: keyof P, value: P[keyof P]): void; - buildFrom(props: P): P | null; + build( + props: Readonly

, + options?: { + includeUndefined?: boolean; + target?: ValueProcessorContext['target']; + } + ): AnyRecord; }; type PropertyValueConfigBase

= diff --git a/packages/react-native-reanimated/src/css/native/__tests__/registry.test.ts b/packages/react-native-reanimated/src/css/native/__tests__/registry.test.ts index 1bd99e4de942..de64390aa584 100644 --- a/packages/react-native-reanimated/src/css/native/__tests__/registry.test.ts +++ b/packages/react-native-reanimated/src/css/native/__tests__/registry.test.ts @@ -5,7 +5,7 @@ import { getPropsBuilder, hasPropsBuilder, registerComponentPropsBuilder, -} from '../registry'; +} from '../../../common/style'; describe('registry', () => { describe('hasPropsBuilder', () => { diff --git a/packages/react-native-reanimated/src/css/native/index.ts b/packages/react-native-reanimated/src/css/native/index.ts index 22caa3f07256..6e8b3d3bb890 100644 --- a/packages/react-native-reanimated/src/css/native/index.ts +++ b/packages/react-native-reanimated/src/css/native/index.ts @@ -3,5 +3,4 @@ export * from './keyframes'; export * from './managers'; export * from './normalization'; export * from './proxy'; -export * from './registry'; export type * from './types'; diff --git a/packages/react-native-reanimated/src/css/native/managers/CSSManager.ts b/packages/react-native-reanimated/src/css/native/managers/CSSManager.ts index 66a68c179e73..65f145dc5ad9 100644 --- a/packages/react-native-reanimated/src/css/native/managers/CSSManager.ts +++ b/packages/react-native-reanimated/src/css/native/managers/CSSManager.ts @@ -6,7 +6,7 @@ import type { CSSStyle } from '../../types'; import type { ICSSManager } from '../../types/interfaces'; import { filterCSSAndStyleProperties } from '../../utils'; import { setViewStyle } from '../proxy'; -import { getPropsBuilder, hasPropsBuilder } from '../registry'; +import { getPropsBuilder, hasPropsBuilder } from '../../../common/style'; import CSSAnimationsManager from './CSSAnimationsManager'; import CSSTransitionsManager from './CSSTransitionsManager'; diff --git a/packages/react-native-reanimated/src/css/native/normalization/animation/__tests__/keyframes.test.ts b/packages/react-native-reanimated/src/css/native/normalization/animation/__tests__/keyframes.test.ts index b001e4e7671f..88fb55c6e091 100644 --- a/packages/react-native-reanimated/src/css/native/normalization/animation/__tests__/keyframes.test.ts +++ b/packages/react-native-reanimated/src/css/native/normalization/animation/__tests__/keyframes.test.ts @@ -1,7 +1,7 @@ 'use strict'; import { ReanimatedError } from '../../../../../common'; import type { Repeat } from '../../../../types'; -import { getPropsBuilder } from '../../../registry'; +import { getPropsBuilder } from '../../../../../common/style'; import { ERROR_MESSAGES, normalizeAnimationKeyframes, diff --git a/packages/react-native-reanimated/src/css/native/normalization/animation/keyframes.ts b/packages/react-native-reanimated/src/css/native/normalization/animation/keyframes.ts index 1441d0d4b18a..5acaa2fa8049 100644 --- a/packages/react-native-reanimated/src/css/native/normalization/animation/keyframes.ts +++ b/packages/react-native-reanimated/src/css/native/normalization/animation/keyframes.ts @@ -17,7 +17,7 @@ import { normalizeTimingFunction } from '../common'; import { getPropsBuilder, getSeparatelyInterpolatedNestedProperties, -} from '../../registry'; +} from '../../../../common/style'; export const ERROR_MESSAGES = { invalidOffsetType: (selector: CSSAnimationKeyframeSelector) => diff --git a/packages/react-native-reanimated/src/css/svg/init.ts b/packages/react-native-reanimated/src/css/svg/init.ts index 7d92bd13ebb3..aa78db623330 100644 --- a/packages/react-native-reanimated/src/css/svg/init.ts +++ b/packages/react-native-reanimated/src/css/svg/init.ts @@ -1,5 +1,5 @@ 'use strict'; -import { registerComponentPropsBuilder } from '../native'; +import { registerComponentPropsBuilder } from '../../common/style'; import { SVG_CIRCLE_PROPERTIES_CONFIG, SVG_ELLIPSE_PROPERTIES_CONFIG, diff --git a/packages/react-native-reanimated/src/css/svg/native/configs/common.ts b/packages/react-native-reanimated/src/css/svg/native/configs/common.ts index d9c5dc3311f2..5870568062a9 100644 --- a/packages/react-native-reanimated/src/css/svg/native/configs/common.ts +++ b/packages/react-native-reanimated/src/css/svg/native/configs/common.ts @@ -16,7 +16,7 @@ import type { TransformProps, } from 'react-native-svg'; -import type { PropsBuilderConfig } from '../../../../common'; +import type { PropsBuilderConfig } from '../../../../common/style'; import { convertStringToNumber, processColorSVG,