diff --git a/components/native/Animated/package.json b/components/native/Animated/package.json new file mode 100644 index 00000000..49a04f33 --- /dev/null +++ b/components/native/Animated/package.json @@ -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" +} diff --git a/cxx/core/UnistylesRegistry.cpp b/cxx/core/UnistylesRegistry.cpp index 6ab24a39..685888e5 100644 --- a/cxx/core/UnistylesRegistry.cpp +++ b/cxx/core/UnistylesRegistry.cpp @@ -86,6 +86,27 @@ void core::UnistylesRegistry::linkShadowNodeWithUnistyle( this->trafficController.resumeUnistylesTraffic(); } +void core::UnistylesRegistry::removeDuplicatedUnistyles(jsi::Runtime& rt, const ShadowNodeFamily *shadowNodeFamily, std::vector& 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& 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); @@ -201,16 +222,16 @@ std::vector> 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; } diff --git a/cxx/core/UnistylesRegistry.h b/cxx/core/UnistylesRegistry.h index f08fcf94..a2f0d6e4 100644 --- a/cxx/core/UnistylesRegistry.h +++ b/cxx/core/UnistylesRegistry.h @@ -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 getScopedTheme(); + void removeDuplicatedUnistyles(jsi::Runtime& rt, const ShadowNodeFamily* shadowNodeFamily, std::vector& unistyles); void setScopedTheme(std::optional themeName); core::Unistyle::Shared getUnistyleById(jsi::Runtime& rt, std::string unistyleID); diff --git a/cxx/hybridObjects/HybridShadowRegistry.cpp b/cxx/hybridObjects/HybridShadowRegistry.cpp index cbca2349..876dbf64 100644 --- a/cxx/hybridObjects/HybridShadowRegistry.cpp +++ b/cxx/hybridObjects/HybridShadowRegistry.cpp @@ -12,6 +12,13 @@ jsi::Value HybridShadowRegistry::link(jsi::Runtime &rt, const jsi::Value &thisVa std::vector> 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 { diff --git a/package.json b/package.json index 5a2876b1..13078d7d 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/plugin/__tests__/playground.js b/plugin/__tests__/playground.js new file mode 100644 index 00000000..840f4dbc --- /dev/null +++ b/plugin/__tests__/playground.js @@ -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) diff --git a/plugin/consts.js b/plugin/consts.js new file mode 100644 index 00000000..dd911f8a --- /dev/null +++ b/plugin/consts.js @@ -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 +} diff --git a/plugin/exotic.js b/plugin/exotic.js new file mode 100644 index 00000000..50c6ca42 --- /dev/null +++ b/plugin/exotic.js @@ -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 +} diff --git a/plugin/index.js b/plugin/index.js index acdb6d42..6367e596 100644 --- a/plugin/index.js +++ b/plugin/index.js @@ -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 { @@ -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 } @@ -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 } }) diff --git a/src/components/native/Animated.tsx b/src/components/native/Animated.tsx new file mode 100644 index 00000000..1ca9daf6 --- /dev/null +++ b/src/components/native/Animated.tsx @@ -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) +} diff --git a/src/components/native/Pressable.native.tsx b/src/components/native/Pressable.native.tsx index 3534e2ca..b51b5fd2 100644 --- a/src/components/native/Pressable.native.tsx +++ b/src/components/native/Pressable.native.tsx @@ -8,6 +8,22 @@ type PressableProps = Props & { variants?: Record } +const getStyles = (styleProps: Record = {}) => { + 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(({ variants, style, ...props }, forwardedRef) => { const storedRef = useRef() @@ -28,7 +44,7 @@ export const Pressable = forwardRef(({ 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) { @@ -40,12 +56,14 @@ export const Pressable = forwardRef(({ variants, style, .. style={state => { const unistyles = typeof style === 'function' ? style(state) - : style + : getStyles(style as unknown as Record) 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) diff --git a/yarn.lock b/yarn.lock index 73ba4fce..b0cb37fa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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: @@ -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: @@ -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