Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for React Native Animated #530

Merged
merged 2 commits into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions components/native/Animated/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"main": "../../../lib/commonjs/components/native/Animated.js",
"module": "../../../lib/module/components/native/Animated.js",
"react-native": "../../../src/components/native/Animated.tsx"
}
27 changes: 24 additions & 3 deletions cxx/core/UnistylesRegistry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,27 @@ void core::UnistylesRegistry::linkShadowNodeWithUnistyle(
this->trafficController.resumeUnistylesTraffic();
}

void core::UnistylesRegistry::removeDuplicatedUnistyles(jsi::Runtime& rt, const ShadowNodeFamily *shadowNodeFamily, std::vector<core::Unistyle::Shared>& unistyles) {
auto targetFamilyUnistyles = this->_shadowRegistry[&rt][shadowNodeFamily];

unistyles.erase(
std::remove_if(
unistyles.begin(),
unistyles.end(),
[&targetFamilyUnistyles](const core::Unistyle::Shared& unistyle) {
return std::any_of(
targetFamilyUnistyles.begin(),
targetFamilyUnistyles.end(),
[&unistyle](const std::shared_ptr<core::UnistyleData>& data) {
return data->unistyle == unistyle;
}
);
}
),
unistyles.end()
);
}

void core::UnistylesRegistry::unlinkShadowNodeWithUnistyles(jsi::Runtime& rt, const ShadowNodeFamily* shadowNodeFamily) {
this->_shadowRegistry[&rt].erase(shadowNodeFamily);
this->trafficController.removeShadowNode(shadowNodeFamily);
Expand Down Expand Up @@ -201,16 +222,16 @@ std::vector<std::shared_ptr<core::StyleSheet>> core::UnistylesRegistry::getStyle
core::Unistyle::Shared core::UnistylesRegistry::getUnistyleById(jsi::Runtime& rt, std::string unistyleID) {
for (auto& pair: this->_styleSheetRegistry[&rt]) {
auto [_, stylesheet] = pair;

for (auto unistylePair: stylesheet->unistyles) {
auto [_, unistyle] = unistylePair;

if (unistyle->unid == unistyleID) {
return unistyle;
}
}
}

return nullptr;
}

Expand Down
1 change: 1 addition & 0 deletions cxx/core/UnistylesRegistry.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ struct UnistylesRegistry: public StyleSheetRegistry {
void shadowLeafUpdateFromUnistyle(jsi::Runtime& rt, Unistyle::Shared unistyle, jsi::Value& maybePressableId);
shadow::ShadowTrafficController trafficController{};
const std::optional<std::string> getScopedTheme();
void removeDuplicatedUnistyles(jsi::Runtime& rt, const ShadowNodeFamily* shadowNodeFamily, std::vector<core::Unistyle::Shared>& unistyles);
void setScopedTheme(std::optional<std::string> themeName);
core::Unistyle::Shared getUnistyleById(jsi::Runtime& rt, std::string unistyleID);

Expand Down
7 changes: 7 additions & 0 deletions cxx/hybridObjects/HybridShadowRegistry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ jsi::Value HybridShadowRegistry::link(jsi::Runtime &rt, const jsi::Value &thisVa
std::vector<std::vector<folly::dynamic>> arguments;
auto& registry = core::UnistylesRegistry::get();

// this is special case for Animated, and prevents appending same unistyles to node
registry.removeDuplicatedUnistyles(rt, &shadowNodeWrapper->getFamily(), unistyleWrappers);

if (unistyleWrappers.empty()) {
return jsi::Value::undefined();
}

for (size_t i = 0; i < unistyleWrappers.size(); i++) {
if (unistyleWrappers[i]->type == core::UnistyleType::DynamicFunction) {
try {
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@
},
"devDependencies": {
"@babel/plugin-syntax-jsx": "7.25.9",
"@babel/preset-flow": "7.25.9",
"@babel/preset-typescript": "7.26.0",
"@biomejs/biome": "1.9.4",
"@commitlint/config-conventional": "19.6.0",
"@react-native/normalize-colors": "0.77.0",
Expand Down
12 changes: 12 additions & 0 deletions plugin/__tests__/playground.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const babel = require('@babel/core')
const plugin = require('../index.js')

const filePath = '../../expo-example/app/(tabs)/index.tsx'

const result = babel.transformFileSync(filePath, {
presets: ['@babel/preset-typescript', '@babel/preset-flow'],
plugins: [plugin],
filename: filePath,
})

console.log(result.code)
43 changes: 43 additions & 0 deletions plugin/consts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
const REACT_NATIVE_COMPONENT_NAMES = [
'ActivityIndicator',
'View',
'Text',
'Image',
'ImageBackground',
'KeyboardAvoidingView',
'Pressable',
'ScrollView',
'FlatList',
'SectionList',
'Switch',
'TextInput',
'RefreshControl',
'TouchableHighlight',
'TouchableOpacity',
'VirtualizedList',
'Animated'
// Modal - there is no exposed native handle
// TouchableWithoutFeedback - can't accept a ref
]

// auto replace RN imports to Unistyles imports under these paths
// our implementation simply borrows 'ref' to register it in ShadowRegistry
// so we won't affect anyone's implementation
const REPLACE_WITH_UNISTYLES_PATHS = [
'react-native-reanimated/src/component',
'react-native-gesture-handler/src/components'
]

// this is more powerful API as it allows to convert unmatched imports to Unistyles
// { path: string, imports: Array<{ name: string, isDefault: boolean, path: string, mapTo: string }> }
// name <- target import name
// isDefault <- is the import default?
// path <- path to the target import
// mapTo <- name of the Unistyles component
const REPLACE_WITH_UNISTYLES_EXOTIC_PATHS = []

module.exports = {
REACT_NATIVE_COMPONENT_NAMES,
REPLACE_WITH_UNISTYLES_PATHS,
REPLACE_WITH_UNISTYLES_EXOTIC_PATHS
}
42 changes: 42 additions & 0 deletions plugin/exotic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
function handleExoticImport(t, path, state, exoticImport) {
const specifiers = path.node.specifiers
const source = path.node.source

if (path.node.importKind !== 'value') {
return
}

specifiers.forEach(specifier => {
for (const rule of exoticImport.imports) {
const hasMatchingImportType = !rule.isDefault || t.isImportDefaultSpecifier(specifier)
const hasMatchingImportName = rule.name === specifier.local.name
const hasMatchingPath = rule.path === source.value

if (!hasMatchingImportType || !hasMatchingImportName || !hasMatchingPath) {
continue
}

const newImport = t.importDeclaration(
[t.importSpecifier(t.identifier(rule.mapTo), t.identifier(rule.mapTo))],
t.stringLiteral(state.opts.isLocal
? state.file.opts.filename.split('react-native-unistyles').at(0).concat(`react-native-unistyles/components/native/${rule.mapTo}`)
: `react-native-unistyles/components/native/${rule.mapTo}`
)
)

// remove old import
if (t.isImportDefaultSpecifier(specifier)) {
path.replaceWith(newImport)
} else {
path.node.specifiers = specifiers.filter(s => s !== specifier)
path.unshift(newImport)
}

return
}
})
}

module.exports = {
handleExoticImport
}
40 changes: 10 additions & 30 deletions plugin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,8 @@ const { addUnistylesImport, isInsideNodeModules } = require('./import')
const { hasStringRef } = require('./ref')
const { isUnistylesStyleSheet, analyzeDependencies, addStyleSheetTag, getUnistyles, isKindOfStyleSheet } = require('./stylesheet')
const { extractVariants } = require('./variants')

const reactNativeComponentNames = [
'ActivityIndicator',
'View',
'Text',
'Image',
'ImageBackground',
'KeyboardAvoidingView',
'Pressable',
'ScrollView',
'FlatList',
'SectionList',
'Switch',
'TextInput',
'RefreshControl',
'TouchableHighlight',
'TouchableOpacity',
'VirtualizedList',
// Modal - there is no exposed native handle
// TouchableWithoutFeedback - can't accept a ref
]

// auto replace RN imports to Unistyles imports under these paths
// our implementation simply borrows 'ref' to register it in ShadowRegistry
// so we won't affect anyone's implementation
const REPLACE_WITH_UNISTYLES_PATHS = [
'react-native-reanimated/src/component',
'react-native-gesture-handler/src/components'
]
const { REACT_NATIVE_COMPONENT_NAMES, REPLACE_WITH_UNISTYLES_PATHS, REPLACE_WITH_UNISTYLES_EXOTIC_PATHS } = require('./consts')
const { handleExoticImport } = require('./exotic')

module.exports = function ({ types: t }) {
return {
Expand Down Expand Up @@ -107,6 +80,13 @@ module.exports = function ({ types: t }) {
},
/** @param {import('./index').UnistylesPluginPass} state */
ImportDeclaration(path, state) {
const exoticImport = REPLACE_WITH_UNISTYLES_EXOTIC_PATHS
.find(exotic => state.filename.includes(exotic.path))

if (exoticImport) {
return handleExoticImport(t, path, state, exoticImport)
}

if (isInsideNodeModules(state) && !state.file.replaceWithUnistyles) {
return
}
Expand All @@ -123,7 +103,7 @@ module.exports = function ({ types: t }) {

if (importSource === 'react-native') {
path.node.specifiers.forEach(specifier => {
if (specifier.imported && reactNativeComponentNames.includes(specifier.imported.name)) {
if (specifier.imported && REACT_NATIVE_COMPONENT_NAMES.includes(specifier.imported.name)) {
state.reactNativeImports[specifier.local.name] = specifier.imported.name
}
})
Expand Down
17 changes: 17 additions & 0 deletions src/components/native/Animated.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Animated as RNAnimated } from 'react-native'
import { View } from './View'
import { Text } from './Text'
import { FlatList } from './FlatList'
import { Image } from './Image'
import { ScrollView } from './ScrollView'
import { SectionList } from './SectionList'

export const Animated = {
...RNAnimated,
View: RNAnimated.createAnimatedComponent(View),
Text: RNAnimated.createAnimatedComponent(Text),
FlatList: RNAnimated.createAnimatedComponent(FlatList),
Image: RNAnimated.createAnimatedComponent(Image),
ScrollView: RNAnimated.createAnimatedComponent(ScrollView),
SectionList: RNAnimated.createAnimatedComponent(SectionList)
}
22 changes: 20 additions & 2 deletions src/components/native/Pressable.native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,22 @@ type PressableProps = Props & {
variants?: Record<string, string | boolean>
}

const getStyles = (styleProps: Record<string, any> = {}) => {
const unistyleKey = Object
.keys(styleProps)
.find(key => key.startsWith('unistyles-'))

if (!unistyleKey) {
return styleProps
}

return {
// styles without C++ state
...styleProps[unistyleKey].uni__getStyles(),
[unistyleKey]: styleProps[unistyleKey].uni__getStyles()
}
}

export const Pressable = forwardRef<View, PressableProps>(({ variants, style, ...props }, forwardedRef) => {
const storedRef = useRef<View | null>()

Expand All @@ -28,7 +44,7 @@ export const Pressable = forwardRef<View, PressableProps>(({ variants, style, ..
? style({ pressed: false })
: style

// @ts-expect-error web types are not compatible with RN styles
// @ts-expect-error - this is hidden from TS
UnistylesShadowRegistry.add(ref, unistyles)

if (ref) {
Expand All @@ -40,12 +56,14 @@ export const Pressable = forwardRef<View, PressableProps>(({ variants, style, ..
style={state => {
const unistyles = typeof style === 'function'
? style(state)
: style
: getStyles(style as unknown as Record<string, any>)

if (!storedRef.current) {
return unistyles
}

// @ts-expect-error - this is hidden from TS
UnistylesShadowRegistry.remove(storedRef.current)
// @ts-expect-error - this is hidden from TS
UnistylesShadowRegistry.add(storedRef.current, unistyles)

Expand Down
8 changes: 7 additions & 1 deletion src/web/utils/unistyle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,13 @@ export const extractSecrets = (object: any) => {
return undefined
}

return reduceObject(Object.getOwnPropertyDescriptors(secrets), secret => secret.value)
const hiddenSecrets = Object.getOwnPropertyDescriptors(secrets)

if (Object.keys(hiddenSecrets).length === 0) {
return undefined
}

return reduceObject(hiddenSecrets, secret => secret.value)
}

export const removeInlineStyles = (values: UnistylesValues) => {
Expand Down
6 changes: 4 additions & 2 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1754,7 +1754,7 @@ __metadata:
languageName: node
linkType: hard

"@babel/preset-flow@npm:^7.13.13, @babel/preset-flow@npm:^7.24.7":
"@babel/preset-flow@npm:7.25.9, @babel/preset-flow@npm:^7.13.13, @babel/preset-flow@npm:^7.24.7":
version: 7.25.9
resolution: "@babel/preset-flow@npm:7.25.9"
dependencies:
Expand Down Expand Up @@ -1796,7 +1796,7 @@ __metadata:
languageName: node
linkType: hard

"@babel/preset-typescript@npm:^7.13.0, @babel/preset-typescript@npm:^7.16.7, @babel/preset-typescript@npm:^7.23.0, @babel/preset-typescript@npm:^7.24.7":
"@babel/preset-typescript@npm:7.26.0, @babel/preset-typescript@npm:^7.13.0, @babel/preset-typescript@npm:^7.16.7, @babel/preset-typescript@npm:^7.23.0, @babel/preset-typescript@npm:^7.24.7":
version: 7.26.0
resolution: "@babel/preset-typescript@npm:7.26.0"
dependencies:
Expand Down Expand Up @@ -15417,6 +15417,8 @@ __metadata:
resolution: "react-native-unistyles@workspace:."
dependencies:
"@babel/plugin-syntax-jsx": 7.25.9
"@babel/preset-flow": 7.25.9
"@babel/preset-typescript": 7.26.0
"@biomejs/biome": 1.9.4
"@commitlint/config-conventional": 19.6.0
"@react-native/normalize-colors": 0.77.0
Expand Down