Skip to content

Commit

Permalink
feat: add support for React Native Aniamted
Browse files Browse the repository at this point in the history
  • Loading branch information
jpudysz committed Jan 28, 2025
1 parent 406a156 commit bb35657
Show file tree
Hide file tree
Showing 12 changed files with 187 additions and 37 deletions.
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
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

0 comments on commit bb35657

Please sign in to comment.