Skip to content

Commit

Permalink
feat: Create HybridView code generator base (#494)
Browse files Browse the repository at this point in the history
* feat: Create `HybridView` Swift and Kotlin base

* fix: HybridView

* feat: Add `HybridView.ts` to nitrogen

* feat: Add necessary changes to nitrogen

* fix: Actually resolve type to find base types (HybridView -> HybridObject)

* feat: Generate TestView.nitro.ts 🎉

* fix: Fix hybrid view base accidentally

* fix: Pass `HybridData` to super

* fix: Finally fix `HybridView` recursion

* fix: Make `HybridView` the protocol's base instead

* Always inherit HybridObject

* run nitrogen

* feat: Create props file

* Name it component

* fix: Indent

* Add warn for RN 78 requirement

* move into `views`

* Create `.mm` file as well

* Add super.load

* fix: Fix namespace

* fix: indent

* fix it?

* Update NitroDefines.hpp

* fix: Make `/views/` private header

* Add filter func and some docs

* feat: More docs

* sort

* feat: Also create Android part

* feat: Implement actual prop parsing

* ah fix

* fix: Runtime type

* fix: Add `hashString(string_view)` equivalent

* fix: Add string_view and string overloads

* fix: indent shadownode

* whoops

* fix: Wrap in state

* comments

* fix: Fix throw when RN isnt present

* fix: Fix `Any?` type

* fix: Includes

* feat: Autolink it and implement it..?

* fix: Load native part of `NitroImage` in shared OnLoad.kt file

* fix: Actually add view-manager to Package

* fix: Rename to Hybrid* prefix

* fix: Use the `*OnLoad` init func now

* fix: Remove duplicate JNI registrations
  • Loading branch information
mrousavy authored Jan 15, 2025
1 parent b387586 commit 3387184
Show file tree
Hide file tree
Showing 59 changed files with 1,748 additions and 155 deletions.
4 changes: 2 additions & 2 deletions docs/docs/nitrogen.md
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ Inside `NitroMathPackage.java`, add:
public class NitroMathPackage extends TurboReactPackage {
// ...
static {
System.loadLibrary("NitroMath");
NitroMathOnLoad.initializeNative();
}
}
```
Expand All @@ -319,7 +319,7 @@ class MainApplication {
// ...
companion object {
init {
System.loadLibrary("NitroMath")
NitroMathOnLoad.initializeNative()
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1894,7 +1894,7 @@ SPEC CHECKSUMS:
fmt: 10c6e61f4be25dc963c36bd73fc7b1705fe975be
glog: 08b301085f15bcbb6ff8632a8ebaf239aae04e6a
hermes-engine: 1949ca944b195a8bde7cbf6316b9068e19cf53c6
NitroImage: ff97c5986ea4619abd3d6399b886eac84f5a4b65
NitroImage: ccc116b3881f723f1d3f77743acf8028681160f1
NitroModules: 9d5bc0172f6d9b098eb29e8cd9891e16881cd4b6
RCT-Folly: bf5c0376ffe4dd2cf438dcf86db385df9fdce648
RCTDeprecation: 063fc281b30b7dc944c98fe53a7e266dab1a8706
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ import { getBuildingWithGeneratedCmakeDefinition } from './createCMakeExtension.

export function createHybridObjectIntializer(): SourceFile[] {
const cxxNamespace = NitroConfig.getCxxNamespace('c++')
const cppLibName = NitroConfig.getAndroidCxxLibName()
const javaNamespace = NitroConfig.getAndroidPackage('java/kotlin')
const autolinkingClassName = `${NitroConfig.getAndroidCxxLibName()}OnLoad`

const jniRegistrations = getJNINativeRegistrations().map(
(r) => `${r.namespace}::${r.className}::registerNatives();`
)
const jniRegistrations = getJNINativeRegistrations()
.map((r) => `${r.namespace}::${r.className}::registerNatives();`)
.filter(isNotDuplicate)

const autolinkedHybridObjects = NitroConfig.getAutolinkedHybridObjects()

Expand Down Expand Up @@ -64,7 +66,7 @@ ${createFileMetadataString(`${autolinkingClassName}.hpp`)}
namespace ${cxxNamespace} {
/**
* Initializes the native (C++) part of ${NitroConfig.getAndroidCxxLibName()}, and autolinks all Hybrid Objects.
* Initializes the native (C++) part of ${cppLibName}, and autolinks all Hybrid Objects.
* Call this in your \`JNI_OnLoad\` function (probably inside \`cpp-adapter.cpp\`).
* Example:
* \`\`\`cpp (cpp-adapter.cpp)
Expand Down Expand Up @@ -110,7 +112,39 @@ int initialize(JavaVM* vm) {
}
} // namespace ${cxxNamespace}
`.trim()

const kotlinCode = `
${createFileMetadataString(`${autolinkingClassName}.kt`)}
package ${javaNamespace}
import android.util.Log
internal class ${autolinkingClassName} {
companion object {
private const val TAG = "${autolinkingClassName}"
private var didLoad = false
/**
* Initializes the native part of "${cppLibName}".
* This method is idempotent and can be called more than once.
*/
@JvmStatic
fun initializeNative() {
if (didLoad) return
try {
Log.i(TAG, "Loading ${cppLibName} C++ library...")
System.loadLibrary("${cppLibName}")
Log.i(TAG, "Successfully loaded ${cppLibName} C++ library!")
didLoad = true
} catch (e: Error) {
Log.e(TAG, "Failed to load ${cppLibName} C++ library! Is it properly installed and linked? " +
"Is the name correct? (see \`CMakeLists.txt\`, at \`add_library(...)\`)", e)
throw e
}
}
}
}
`.trim()

return [
Expand All @@ -128,5 +162,12 @@ int initialize(JavaVM* vm) {
platform: 'android',
subdirectory: [],
},
{
content: kotlinCode,
language: 'kotlin',
name: `${autolinkingClassName}.kt`,
platform: 'android',
subdirectory: ['kotlin', ...javaNamespace.split('.')],
},
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ def add_nitrogen_files(spec)
spec.private_header_files = current_private_header_files + [
# iOS specific specs
"nitrogen/generated/ios/c++/**/*.{h,hpp}",
# Views are framework-specific and should be private
"nitrogen/generated/shared/**/views/**/*"
]
current_pod_target_xcconfig = spec.attributes_hash['pod_target_xcconfig'] || {}
Expand Down
10 changes: 7 additions & 3 deletions packages/nitrogen/src/createPlatformSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import type { SourceFile } from './syntax/SourceFile.js'
import { createCppHybridObject } from './syntax/c++/CppHybridObject.js'
import {
extendsHybridObject,
extendsHybridView,
isAnyHybridSubclass,
isDirectlyHybridObject,
type Language,
} from './getPlatformSpecs.js'
Expand All @@ -13,6 +15,7 @@ import { createSwiftHybridObject } from './syntax/swift/SwiftHybridObject.js'
import { createKotlinHybridObject } from './syntax/kotlin/KotlinHybridObject.js'
import { createType } from './syntax/createType.js'
import { Parameter } from './syntax/Parameter.js'
import { getBaseTypes } from './utils.js'

export function generatePlatformFiles(
interfaceType: Type,
Expand Down Expand Up @@ -103,17 +106,18 @@ function getHybridObjectSpec(type: Type, language: Language): HybridObjectSpec {
}
}

const bases = type
.getBaseTypes()
.filter((t) => extendsHybridObject(t, false))
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,
}
return spec
}
Expand Down
68 changes: 48 additions & 20 deletions packages/nitrogen/src/getPlatformSpecs.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { PlatformSpec } from 'react-native-nitro-modules'
import type { InterfaceDeclaration, Type } from 'ts-morph'
import { Symbol } from 'ts-morph'
import { getBaseTypes } from './utils.js'

export type Platform = keyof Required<PlatformSpec>
export type Language = Required<PlatformSpec>[keyof PlatformSpec]
Expand Down Expand Up @@ -86,22 +87,22 @@ function getPlatformSpec(typeName: string, platformSpecs: Type): PlatformSpec {
return result
}

export function isDirectlyHybridObject(type: Type): boolean {
function isDirectlyType(type: Type, name: string): boolean {
const symbol = type.getSymbol() ?? type.getAliasSymbol()
if (symbol?.getName() === 'HybridObject') {
if (symbol?.getName() === name) {
return true
}
return false
}

export function extendsHybridObject(type: Type, recursive: boolean): boolean {
for (const base of type.getBaseTypes()) {
const isHybrid = isDirectlyHybridObject(base)
function extendsType(type: Type, name: string, recursive: boolean): boolean {
for (const base of getBaseTypes(type)) {
const isHybrid = isDirectlyType(base, name)
if (isHybrid) {
return true
}
if (recursive) {
const baseExtends = extendsHybridObject(base, recursive)
const baseExtends = extendsType(base, name, recursive)
if (baseExtends) {
return true
}
Expand All @@ -110,18 +111,28 @@ export function extendsHybridObject(type: Type, recursive: boolean): boolean {
return false
}

function findHybridObjectBase(type: Type): Type | undefined {
for (const base of type.getBaseTypes()) {
const symbol = base.getSymbol() ?? base.getAliasSymbol()
if (symbol?.getName() === 'HybridObject') {
return base
}
const baseBase = findHybridObjectBase(base)
if (baseBase != null) {
return baseBase
}
}
return undefined
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 isAnyHybridSubclass(type: Type): boolean {
if (isDirectlyHybridObject(type)) return false
if (isDirectlyHybridView(type)) return false

const isCustomHybrid =
extendsHybridObject(type, true) || extendsHybridView(type, true)

return isCustomHybrid
}

/**
Expand All @@ -132,10 +143,14 @@ function findHybridObjectBase(type: Type): Type | undefined {
export function getHybridObjectPlatforms(
declaration: InterfaceDeclaration
): PlatformSpec | undefined {
const base = findHybridObjectBase(declaration.getType())
const base = getBaseTypes(declaration.getType()).find((t) =>
isDirectlyHybridObject(t)
)
if (base == null) {
// this type does not extend `HybridObject`.
return undefined
throw new Error(
`Couldn't find HybridObject<..> base for ${declaration.getName()}! (${declaration.getText()})`
)
}

const genericArguments = base.getTypeArguments()
Expand All @@ -147,3 +162,16 @@ export function getHybridObjectPlatforms(

return getPlatformSpec(declaration.getName(), platformSpecsArgument)
}

export function getHybridViewPlatforms(
view: InterfaceDeclaration
): PlatformSpec | undefined {
const genericArguments = view.getType().getTypeArguments()
const platformSpecsArgument = genericArguments[0]
if (platformSpecsArgument == null) {
// it uses `HybridObject` without generic arguments. This defaults to platform native languages
return { ios: 'swift', android: 'kotlin' }
}

return getPlatformSpec(view.getName(), platformSpecsArgument)
}
43 changes: 32 additions & 11 deletions packages/nitrogen/src/nitrogen.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { Project } from 'ts-morph'
import { getHybridObjectPlatforms, type Platform } from './getPlatformSpecs.js'
import {
extendsHybridObject,
extendsHybridView,
getHybridObjectPlatforms,
getHybridViewPlatforms,
type Platform,
} from './getPlatformSpecs.js'
import { generatePlatformFiles } from './createPlatformSpec.js'
import path from 'path'
import { prettifyDirectory } from './prettifyDirectory.js'
Expand All @@ -18,6 +24,7 @@ import { createIOSAutolinking } from './autolinking/createIOSAutolinking.js'
import { createAndroidAutolinking } from './autolinking/createAndroidAutolinking.js'
import type { Autolinking } from './autolinking/Autolinking.js'
import { createGitAttributes } from './createGitAttributes.js'
import type { PlatformSpec } from 'react-native-nitro-modules'

interface NitrogenOptions {
baseDirectory: string
Expand Down Expand Up @@ -54,15 +61,15 @@ export async function runNitrogen({
project.addSourceFilesAtPaths(globPattern)

// Loop through all source files to log them
console.log(
Logger.info(
chalk.reset(
`🚀 Nitrogen runs at ${chalk.underline(prettifyDirectory(baseDirectory))}`
)
)
for (const dir of project.getDirectories()) {
const specs = dir.getSourceFiles().length
const relativePath = prettifyDirectory(dir.getPath())
console.log(
Logger.info(
` 🔍 Nitrogen found ${specs} spec${specs === 1 ? '' : 's'} in ${chalk.underline(relativePath)}`
)
}
Expand All @@ -72,7 +79,7 @@ export async function runNitrogen({
const searchDir = prettifyDirectory(
path.join(path.resolve(baseDirectory), '**', '*.nitro.ts')
)
console.log(
Logger.error(
`❌ Nitrogen didn't find any spec files in ${chalk.underline(searchDir)}! ` +
`To create a Nitro Module, create a TypeScript file with the "${chalk.underline('.nitro.ts')}" suffix ` +
'and export an interface that extends HybridObject<T>.'
Expand All @@ -85,7 +92,7 @@ export async function runNitrogen({
const writtenFiles: SourceFile[] = []

for (const sourceFile of project.getSourceFiles()) {
console.log(`⏳ Parsing ${sourceFile.getBaseName()}...`)
Logger.info(`⏳ Parsing ${sourceFile.getBaseName()}...`)

const startedWithSpecs = generatedSpecs

Expand All @@ -94,25 +101,39 @@ export async function runNitrogen({
for (const declaration of interfaceDeclarations) {
let typeName = declaration.getName()
try {
// Find out if it extends HybridObject
const platformSpec = getHybridObjectPlatforms(declaration)
if (platformSpec == null) {
// It does not extend HybridObject, continue..
let platformSpec: PlatformSpec
if (extendsHybridView(declaration.getType(), true)) {
// Hybrid View Props
const targetPlatforms = getHybridViewPlatforms(declaration)
if (targetPlatforms == null) {
// It does not extend HybridView, continue..
continue
}
platformSpec = targetPlatforms
} else if (extendsHybridObject(declaration.getType(), true)) {
// Hybrid View
const targetPlatforms = getHybridObjectPlatforms(declaration)
if (targetPlatforms == null) {
// It does not extend HybridObject, continue..
continue
}
platformSpec = targetPlatforms
} else {
continue
}

const platforms = Object.keys(platformSpec) as Platform[]

if (platforms.length === 0) {
console.warn(
Logger.warn(
`⚠️ ${typeName} does not declare any platforms in HybridObject<T> - nothing can be generated.`
)
continue
}

targetSpecs++

console.log(
Logger.info(
` ⚙️ Generating specs for HybridObject "${chalk.bold(typeName)}"...`
)

Expand Down
1 change: 1 addition & 0 deletions packages/nitrogen/src/syntax/HybridObjectSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export interface HybridObjectSpec {
properties: Property[]
methods: Method[]
baseTypes: HybridObjectSpec[]
isHybridView: boolean
}
Loading

0 comments on commit 3387184

Please sign in to comment.