Skip to content

Commit

Permalink
feat: Treat HybridViews differently now - they're type aliases (#552)
Browse files Browse the repository at this point in the history
* feat: Refactor `HybridView`: props and methods are separate now

* Update HybridView.ts

* feat: Treat HybridViews differently now - they're type aliases

* impl

* fix types

* fix: Fix default `ViewProps`

* fix: Fix getting name of Hybrid View

* fix: Prepare props parser before `cloneProps`
  • Loading branch information
mrousavy authored Feb 19, 2025
1 parent 29068e4 commit fa8ede8
Show file tree
Hide file tree
Showing 20 changed files with 233 additions and 63 deletions.
29 changes: 26 additions & 3 deletions packages/nitrogen/src/createPlatformSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { SourceFile } from './syntax/SourceFile.js'
import { createCppHybridObject } from './syntax/c++/CppHybridObject.js'
import {
extendsHybridObject,
extendsHybridView,
isHybridView,
isAnyHybridSubclass,
isDirectlyHybridObject,
type Language,
Expand Down Expand Up @@ -41,6 +41,30 @@ export function generatePlatformFiles(
}

function getHybridObjectSpec(type: Type, language: Language): HybridObjectSpec {
if (isHybridView(type)) {
const symbol = type.getAliasSymbolOrThrow()
const name = symbol.getEscapedName()

// It's a Hybrid View - the props & methods are passed as type parameters instead of interface body.
const [props, methods] = type.getTypeArguments()
if (props == null)
throw new Error(
`Props cannot be null! ${name}<...> (HybridView) requires type arguments.`
)
const propsSpec = getHybridObjectSpec(props, language)
const methodsSpec =
methods != null ? getHybridObjectSpec(methods, language) : undefined

return {
baseTypes: [],
isHybridView: true,
language: language,
methods: methodsSpec?.methods ?? [],
properties: propsSpec.properties,
name: name,
}
}

const symbol = type.getSymbolOrThrow()
const name = symbol.getEscapedName()

Expand Down Expand Up @@ -109,15 +133,14 @@ function getHybridObjectSpec(type: Type, language: Language): HybridObjectSpec {
const bases = getBaseTypes(type)
.filter((t) => isAnyHybridSubclass(t))
.map((t) => getHybridObjectSpec(t, language))
const isHybridView = extendsHybridView(type, true)

const spec: HybridObjectSpec = {
language: language,
name: name,
properties: properties,
methods: methods,
baseTypes: bases,
isHybridView: isHybridView,
isHybridView: isHybridView(type),
}
return spec
}
Expand Down
26 changes: 13 additions & 13 deletions packages/nitrogen/src/getPlatformSpecs.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { PlatformSpec } from 'react-native-nitro-modules'
import type { InterfaceDeclaration, Type } from 'ts-morph'
import type { InterfaceDeclaration, Type, TypeAliasDeclaration } from 'ts-morph'
import { Symbol } from 'ts-morph'
import { getBaseTypes } from './utils.js'

Expand Down Expand Up @@ -114,25 +114,25 @@ function extendsType(type: Type, name: string, recursive: boolean): boolean {
export function isDirectlyHybridObject(type: Type): boolean {
return isDirectlyType(type, 'HybridObject')
}
export function isDirectlyHybridView(type: Type): boolean {
return isDirectlyType(type, 'HybridView')
}

export function extendsHybridObject(type: Type, recursive: boolean): boolean {
return extendsType(type, 'HybridObject', recursive)
}
export function extendsHybridView(type: Type, recursive: boolean): boolean {
return extendsType(type, 'HybridView', recursive)
export function isHybridView(type: Type): boolean {
// HybridViews are type aliases for `HybridView`
const symbol = type.getSymbol()
if (symbol == null) return false
return symbol.getName() === 'HybridView'
}

export function isAnyHybridSubclass(type: Type): boolean {
if (isDirectlyHybridObject(type)) return false
if (isDirectlyHybridView(type)) return false

const isCustomHybrid =
extendsHybridObject(type, true) || extendsHybridView(type, true)
if (isHybridView(type)) return true

if (extendsHybridObject(type, true)) return true

return isCustomHybrid
return false
}

/**
Expand All @@ -141,7 +141,7 @@ export function isAnyHybridSubclass(type: Type): boolean {
* If it doesn't extend `HybridObject`, this returns `undefined`.
*/
export function getHybridObjectPlatforms(
declaration: InterfaceDeclaration
declaration: InterfaceDeclaration | TypeAliasDeclaration
): PlatformSpec | undefined {
const base = getBaseTypes(declaration.getType()).find((t) =>
isDirectlyHybridObject(t)
Expand All @@ -164,10 +164,10 @@ export function getHybridObjectPlatforms(
}

export function getHybridViewPlatforms(
view: InterfaceDeclaration
view: InterfaceDeclaration | TypeAliasDeclaration
): PlatformSpec | undefined {
const genericArguments = view.getType().getTypeArguments()
const platformSpecsArgument = genericArguments[0]
const platformSpecsArgument = genericArguments[2]
if (platformSpecsArgument == null) {
// it uses `HybridObject` without generic arguments. This defaults to platform native languages
return { ios: 'swift', android: 'kotlin' }
Expand Down
11 changes: 7 additions & 4 deletions packages/nitrogen/src/nitrogen.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Project } from 'ts-morph'
import {
extendsHybridObject,
extendsHybridView,
isHybridView,
getHybridObjectPlatforms,
getHybridViewPlatforms,
type Platform,
Expand Down Expand Up @@ -97,12 +97,15 @@ export async function runNitrogen({
const startedWithSpecs = generatedSpecs

// Find all interfaceDeclarations in the given file
const interfaceDeclarations = sourceFile.getInterfaces()
for (const declaration of interfaceDeclarations) {
const declarations = [
...sourceFile.getInterfaces(),
...sourceFile.getTypeAliases(),
]
for (const declaration of declarations) {
let typeName = declaration.getName()
try {
let platformSpec: PlatformSpec
if (extendsHybridView(declaration.getType(), true)) {
if (isHybridView(declaration.getType())) {
// Hybrid View Props
const targetPlatforms = getHybridViewPlatforms(declaration)
if (targetPlatforms == null) {
Expand Down
13 changes: 12 additions & 1 deletion packages/nitrogen/src/syntax/createType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { TupleType } from './types/TupleType.js'
import {
isAnyHybridSubclass,
isDirectlyHybridObject,
isHybridView,
type Language,
} from '../getPlatformSpecs.js'
import { HybridObjectBaseType } from './types/HybridObjectBaseType.js'
Expand Down Expand Up @@ -61,6 +62,16 @@ function isError(type: TSMorphType): boolean {
return isSymbol(type, 'Error')
}

function getHybridObjectName(type: TSMorphType): string {
const symbol = isHybridView(type) ? type.getAliasSymbol() : type.getSymbol()
if (symbol == null) {
throw new Error(
`Cannot get name of \`${type.getText()}\` - symbol not found!`
)
}
return symbol.getEscapedName()
}

function getFunctionCallSignature(func: TSMorphType): Signature {
const callSignatures = func.getCallSignatures()
const callSignature = callSignatures[0]
Expand Down Expand Up @@ -302,7 +313,7 @@ export function createType(
}
} else if (isAnyHybridSubclass(type)) {
// It is another HybridObject being referenced!
const typename = type.getSymbolOrThrow().getEscapedName()
const typename = getHybridObjectName(type)
const baseTypes = getBaseTypes(type)
.filter((t) => isAnyHybridSubclass(t))
.map((b) => createType(language, b, false))
Expand Down
3 changes: 3 additions & 0 deletions packages/nitrogen/src/views/CppHybridViewComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,9 @@ namespace ${namespace} {
react::Props::Shared ${descriptorClassName}::cloneProps(const react::PropsParserContext& context,
${descriptorIndent} const react::Props::Shared& props,
${descriptorIndent} react::RawProps rawProps) const {
// 1. Prepare raw props parser
rawProps.parse(rawPropsParser_);
// 2. Copy props with Nitro's cached copy constructor
return ${shadowNodeClassName}::Props(context, /* & */ rawProps, props);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ class HybridTestView: HybridTestViewSpec() {
_isBlue = value
val color = if (value) Color.BLUE else Color.RED
view.setBackgroundColor(color)
someCallback()
}
override var someCallback: () -> Unit = {}

// Methods
override fun someMethod(): Unit {
someCallback()
}
}
6 changes: 5 additions & 1 deletion packages/react-native-nitro-image/ios/HybridTestView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@ class HybridTestView : HybridTestViewSpec {
var isBlue: Bool = false {
didSet {
view.backgroundColor = isBlue ? .systemBlue : .systemRed
someCallback()
}
}
var someCallback: () -> Void = { }

// Methods
func someMethod() throws -> Void {
someCallback()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ namespace margelo::nitro::image {
}

// Methods

void JHybridTestViewSpec::someMethod() {
static const auto method = _javaPart->getClass()->getMethod<void()>("someMethod");
method(_javaPart);
}

} // namespace margelo::nitro::image
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ namespace margelo::nitro::image {

public:
// Methods

void someMethod() override;

private:
friend HybridBase;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ abstract class HybridTestViewSpec: HybridView() {
}

// Methods

@DoNotStrip
@Keep
abstract fun someMethod(): Unit

private external fun initHybrid(): HybridData

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,12 @@ namespace margelo::nitro::image {

public:
// Methods

inline void someMethod() override {
auto __result = _swiftPart.someMethod();
if (__result.hasError()) [[unlikely]] {
std::rethrow_exception(__result.error());
}
}

private:
NitroImage::HybridTestViewSpec_cxx _swiftPart;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public protocol HybridTestViewSpec_protocol: HybridObject, HybridView {
var someCallback: () -> Void { get set }

// Methods

func someMethod() throws -> Void
}

/// See ``HybridTestViewSpec``
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,17 @@ public class HybridTestViewSpec_cxx {
}

// Methods
@inline(__always)
public final func someMethod() -> bridge.Result_void_ {
do {
try self.__implementation.someMethod()
return bridge.create_Result_void_()
} catch (let __error) {
let __exceptionPtr = __error.toCpp()
return bridge.create_Result_void_(__exceptionPtr)
}
}

public final func getView() -> UnsafeMutableRawPointer {
return Unmanaged.passRetained(__implementation.view).toOpaque()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ namespace margelo::nitro::image {
prototype.registerHybridSetter("isBlue", &HybridTestViewSpec::setIsBlue);
prototype.registerHybridGetter("someCallback", &HybridTestViewSpec::getSomeCallback);
prototype.registerHybridSetter("someCallback", &HybridTestViewSpec::setSomeCallback);
prototype.registerHybridMethod("someMethod", &HybridTestViewSpec::someMethod);
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ namespace margelo::nitro::image {

public:
// Methods

virtual void someMethod() = 0;

protected:
// Hybrid Setup
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ namespace margelo::nitro::image::views {
react::Props::Shared HybridTestViewComponentDescriptor::cloneProps(const react::PropsParserContext& context,
const react::Props::Shared& props,
react::RawProps rawProps) const {
// 1. Prepare raw props parser
rawProps.parse(rawPropsParser_);
// 2. Copy props with Nitro's cached copy constructor
return HybridTestViewShadowNode::Props(context, /* & */ rawProps, props);
}

Expand Down
13 changes: 11 additions & 2 deletions packages/react-native-nitro-image/src/specs/TestView.nitro.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import type { HybridView } from 'react-native-nitro-modules'
import type {
HybridView,
HybridViewMethods,
HybridViewProps,
} from 'react-native-nitro-modules'

export interface TestView extends HybridView {
export interface TestViewProps extends HybridViewProps {
isBlue: boolean
someCallback: () => void
}
export interface TestViewMethods extends HybridViewMethods {
someMethod(): void
}

export type TestView = HybridView<TestViewProps, TestViewMethods>
7 changes: 5 additions & 2 deletions packages/react-native-nitro-image/src/views/TestView.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { getHostComponent } from 'react-native-nitro-modules'
import TestViewConfig from '../../nitrogen/generated/shared/json/TestViewConfig.json'
import { type TestView as TestViewProps } from '../specs/TestView.nitro'
import {
type TestViewMethods,
type TestViewProps,
} from '../specs/TestView.nitro'

/**
* Represents the HybridView `TestView`, which can be rendered as a React Native view.
*/
export const TestView = getHostComponent<TestViewProps>(
export const TestView = getHostComponent<TestViewProps, TestViewMethods>(
'TestView',
() => TestViewConfig
)
Loading

0 comments on commit fa8ede8

Please sign in to comment.