diff --git a/.github/workflows/build-templates.yml b/.github/workflows/build-templates.yml index f2bcad51e..0c1fe3d59 100644 --- a/.github/workflows/build-templates.yml +++ b/.github/workflows/build-templates.yml @@ -35,6 +35,8 @@ jobs: language: kotlin-objc - name: nitro-module language: kotlin-swift + - name: nitro-view + language: kotlin-swift include: - os: ubuntu-latest type: @@ -140,6 +142,11 @@ jobs: fi fi + - name: Generate nitrogen code + if: matrix.type == 'nitro-view' || matrix.type == 'nitro-module' + working-directory: ${{ env.work_dir }} + run: yarn nitrogen + - name: Cache turborepo if: env.android_build == 1 || env.ios_build == 1 uses: actions/cache@v4 diff --git a/packages/create-react-native-library/src/constants.ts b/packages/create-react-native-library/src/constants.ts index 1cecfd74d..583385744 100644 --- a/packages/create-react-native-library/src/constants.ts +++ b/packages/create-react-native-library/src/constants.ts @@ -1,3 +1,3 @@ export const FALLBACK_BOB_VERSION = '0.40.8'; -export const FALLBACK_NITRO_MODULES_VERSION = '0.25.2'; +export const FALLBACK_NITRO_MODULES_VERSION = '0.26.2'; export const SUPPORTED_REACT_NATIVE_VERSION = '0.79.2'; diff --git a/packages/create-react-native-library/src/exampleApp/generateExampleApp.ts b/packages/create-react-native-library/src/exampleApp/generateExampleApp.ts index b8df1856b..b0c7e2530 100644 --- a/packages/create-react-native-library/src/exampleApp/generateExampleApp.ts +++ b/packages/create-react-native-library/src/exampleApp/generateExampleApp.ts @@ -196,7 +196,10 @@ export default async function generateExampleApp({ 'react-native-monorepo-config': `^0.1.9`, }; - if (config.project.moduleConfig === 'nitro-modules') { + if ( + config.project.moduleConfig === 'nitro-modules' || + config.project.viewConfig === 'nitro-view' + ) { const packagesToAddNitro = { 'react-native-nitro-modules': `^${config.versions.nitro || 'latest'}`, }; diff --git a/packages/create-react-native-library/src/index.ts b/packages/create-react-native-library/src/index.ts index 14b95ee26..b10847fb1 100644 --- a/packages/create-react-native-library/src/index.ts +++ b/packages/create-react-native-library/src/index.ts @@ -72,9 +72,8 @@ async function create(_argv: Args) { }); const bobVersion = await bobVersionPromise; - const nitroModulesVersion = - answers.type === 'nitro-module' + answers.type === 'nitro-module' || answers.type === 'nitro-view' ? await nitroModulesVersionPromise : undefined; diff --git a/packages/create-react-native-library/src/input.ts b/packages/create-react-native-library/src/input.ts index 387438548..b17f22231 100644 --- a/packages/create-react-native-library/src/input.ts +++ b/packages/create-react-native-library/src/input.ts @@ -14,6 +14,7 @@ export type ProjectType = | 'turbo-module' | 'fabric-view' | 'nitro-module' + | 'nitro-view' | 'library'; const LANGUAGE_CHOICES: { @@ -24,7 +25,7 @@ const LANGUAGE_CHOICES: { { title: 'Kotlin & Swift', value: 'kotlin-swift', - types: ['nitro-module'], + types: ['nitro-module', 'nitro-view'], }, { title: 'Kotlin & Objective-C', @@ -84,6 +85,12 @@ const TYPE_CHOICES: { description: 'type-safe, fast integration for native APIs to JS (experimental)', }, + { + title: 'Nitro view', + value: 'nitro-view', + description: + 'integration for native views to JS using nitro for prop parsing (experimental)', + }, { title: 'JavaScript library', value: 'library', diff --git a/packages/create-react-native-library/src/template.ts b/packages/create-react-native-library/src/template.ts index bce76ee97..f02a2919c 100644 --- a/packages/create-react-native-library/src/template.ts +++ b/packages/create-react-native-library/src/template.ts @@ -14,7 +14,7 @@ export type ModuleConfig = | 'nitro-modules' | null; -export type ViewConfig = 'paper-view' | 'fabric-view' | null; +export type ViewConfig = 'paper-view' | 'fabric-view' | 'nitro-view' | null; // Please think at least 5 times before introducing a new config key // You can just reuse the existing ones most of the time @@ -76,6 +76,7 @@ const NATIVE_FILES = { module_new: path.resolve(__dirname, '../templates/native-library-new'), view_new: path.resolve(__dirname, '../templates/native-view-new'), module_nitro: path.resolve(__dirname, '../templates/nitro-module'), + view_nitro: path.resolve(__dirname, '../templates/nitro-view'), } as const; const OBJC_FILES = { @@ -155,6 +156,7 @@ function getModuleConfig(projectType: ProjectType): ModuleConfig { return 'turbo-modules'; case 'fabric-view': case 'library': + case 'nitro-view': return null; } } @@ -163,9 +165,12 @@ function getViewConfig(projectType: ProjectType): ViewConfig { switch (projectType) { case 'fabric-view': return 'fabric-view'; + case 'nitro-view': + return 'nitro-view'; case 'nitro-module': case 'turbo-module': case 'library': + default: return null; } } @@ -208,6 +213,11 @@ export async function applyTemplates( return; } + if (config.project.viewConfig === 'nitro-view') { + await applyTemplate(config, NATIVE_FILES['view_nitro'], folder); + return; + } + if (config.project.moduleConfig !== null) { await applyTemplate(config, NATIVE_FILES[`module_new`], folder); } else { diff --git a/packages/create-react-native-library/templates/common/$.github/workflows/ci.yml b/packages/create-react-native-library/templates/common/$.github/workflows/ci.yml index 9fa7e5228..4dcbeeb8e 100644 --- a/packages/create-react-native-library/templates/common/$.github/workflows/ci.yml +++ b/packages/create-react-native-library/templates/common/$.github/workflows/ci.yml @@ -61,7 +61,7 @@ jobs: - name: Setup uses: ./.github/actions/setup -<% if (project.moduleConfig === 'nitro-modules') { -%> +<% if (project.moduleConfig === 'nitro-modules' || project.viewConfig === 'nitro-view') { -%> - name: Generate nitrogen code run: yarn nitrogen @@ -123,7 +123,7 @@ jobs: - name: Setup uses: ./.github/actions/setup -<% if (project.moduleConfig === 'nitro-modules') { -%> +<% if (project.moduleConfig === 'nitro-modules' || project.viewConfig === 'nitro-view') { -%> - name: Generate nitrogen code run: yarn nitrogen diff --git a/packages/create-react-native-library/templates/common/$package.json b/packages/create-react-native-library/templates/common/$package.json index 4a0f1184f..a351f7776 100644 --- a/packages/create-react-native-library/templates/common/$package.json +++ b/packages/create-react-native-library/templates/common/$package.json @@ -48,7 +48,7 @@ "clean": "del-cli lib", <% } -%> "prepare": "bob build", -<% if (project.moduleConfig === 'nitro-modules') { -%> +<% if (project.moduleConfig === 'nitro-modules' || project.viewConfig === 'nitro-view') { -%> "nitrogen": "nitro-codegen", <% } -%> "release": "release-it --only-version" @@ -91,14 +91,14 @@ "eslint-config-prettier": "^10.1.1", "eslint-plugin-prettier": "^5.2.3", "jest": "^29.7.0", -<% if (project.moduleConfig === 'nitro-modules') { -%> +<% if (project.moduleConfig === 'nitro-modules' || project.viewConfig === 'nitro-view') { -%> "nitro-codegen": "^<%- versions.nitro %>", <% } -%> "prettier": "^3.0.3", "react": "19.0.0", "react-native": "0.78.1", "react-native-builder-bob": "^<%- versions.bob %>", -<% if (project.moduleConfig === 'nitro-modules') { -%> +<% if (project.moduleConfig === 'nitro-modules' || project.viewConfig === 'nitro-view') { -%> "react-native-nitro-modules": "^<%- versions.nitro %>", <% } -%> "release-it": "^17.10.0", @@ -109,7 +109,7 @@ }, "peerDependencies": { "react": "*", -<% if (project.moduleConfig === 'nitro-modules') { -%> +<% if (project.moduleConfig === 'nitro-modules' || project.viewConfig === 'nitro-view') { -%> "react-native": "*", "react-native-nitro-modules": "^<%- versions.nitro %>" <% } else { -%> @@ -164,7 +164,7 @@ "source": "src", "output": "lib", "targets": [ -<% if (project.moduleConfig === "nitro-modules") { -%> +<% if (project.moduleConfig === "nitro-modules" || project.viewConfig === "nitro-view") { -%> [ "custom", { diff --git a/packages/create-react-native-library/templates/common/CONTRIBUTING.md b/packages/create-react-native-library/templates/common/CONTRIBUTING.md index ab5549219..2c4d7f103 100644 --- a/packages/create-react-native-library/templates/common/CONTRIBUTING.md +++ b/packages/create-react-native-library/templates/common/CONTRIBUTING.md @@ -19,7 +19,7 @@ yarn > Since the project relies on Yarn workspaces, you cannot use [`npm`](https://github.com/npm/cli) for development. -<% if (project.moduleConfig === 'nitro-modules') { -%> +<% if (project.moduleConfig === 'nitro-modules' || project.viewConfig === 'nitro-view') { -%> This project uses Nitro Modules. If you're not familiar with how Nitro works, make sure to check the [Nitro Modules Docs](https://nitro.margelo.com/). You need to run [Nitrogen](https://nitro.margelo.com/docs/nitrogen) to generate the boilerplate code required for this project. The example app will not build without this step. diff --git a/packages/create-react-native-library/templates/common/README.md b/packages/create-react-native-library/templates/common/README.md index 5b34c58f1..aacb0cf34 100644 --- a/packages/create-react-native-library/templates/common/README.md +++ b/packages/create-react-native-library/templates/common/README.md @@ -4,16 +4,20 @@ ## Installation -<% if (project.moduleConfig === 'nitro-modules') { -%> +<% if (project.moduleConfig === 'nitro-modules' || project.viewConfig === 'nitro-view') { -%> + ```sh npm install <%- project.slug %> react-native-nitro-modules > `react-native-nitro-modules` is required as this library relies on [Nitro Modules](https://nitro.margelo.com/). ``` + <% } else { -%> + ```sh npm install <%- project.slug %> ``` + <% } -%> ## Usage @@ -28,7 +32,7 @@ import { <%- project.name -%>View } from "<%- project.slug -%>"; <<%- project.name -%>View color="tomato" /> ``` -<% } else if (project.moduleConfig === 'nitro-modules' || project.moduleConfig === 'turbo-modules') { -%> +<% } else if (project.moduleConfig === 'nitro-modules' || project.viewConfig === 'nitro-view' || project.moduleConfig === 'turbo-modules') { -%> ```js import { multiply } from '<%- project.slug -%>'; diff --git a/packages/create-react-native-library/templates/native-common/android/build.gradle b/packages/create-react-native-library/templates/native-common/android/build.gradle index e9383fe6a..e742c7f85 100644 --- a/packages/create-react-native-library/templates/native-common/android/build.gradle +++ b/packages/create-react-native-library/templates/native-common/android/build.gradle @@ -15,7 +15,7 @@ buildscript { } } -<% if (project.moduleConfig === 'nitro-modules') { -%> +<% if (project.cpp || project.moduleConfig === 'nitro-modules' || project.viewConfig === 'nitro-view') { -%> def reactNativeArchitectures() { def value = rootProject.getProperties().get("reactNativeArchitectures") return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"] @@ -24,7 +24,7 @@ def reactNativeArchitectures() { apply plugin: "com.android.library" apply plugin: "kotlin-android" -<% if (project.moduleConfig === 'nitro-modules') { -%> +<% if (project.moduleConfig === 'nitro-modules' || project.viewConfig === 'nitro-view') { -%> apply from: '../nitrogen/generated/android/<%- project.package_cpp -%>+autolinking.gradle' <% } -%> @@ -35,7 +35,7 @@ def getExtOrIntegerDefault(name) { } android { -<% if (project.moduleConfig === 'nitro-modules') { -%> +<% if (project.moduleConfig === 'nitro-modules' || project.viewConfig === 'nitro-view') { -%> namespace "com.margelo.nitro.<%- project.package -%>" <% } else { -%> namespace "com.<%- project.package -%>" @@ -46,7 +46,7 @@ android { defaultConfig { minSdkVersion getExtOrIntegerDefault("minSdkVersion") targetSdkVersion getExtOrIntegerDefault("targetSdkVersion") -<% if (project.moduleConfig === 'nitro-modules') { -%> +<% if (project.cpp || project.moduleConfig === 'nitro-modules' || project.viewConfig === 'nitro-view') { -%> externalNativeBuild { cmake { @@ -66,7 +66,7 @@ android { } <% } -%> } -<% if (project.moduleConfig === 'nitro-modules') { -%> +<% if (project.cpp || project.moduleConfig === 'nitro-modules' || project.viewConfig === 'nitro-view') { -%> externalNativeBuild { cmake { @@ -74,7 +74,7 @@ android { } } <% } -%> -<% if (project.moduleConfig === 'nitro-modules') { -%> +<% if (project.moduleConfig === 'nitro-modules' || project.viewConfig === 'nitro-view') { -%> packagingOptions { excludes = [ @@ -100,7 +100,7 @@ android { buildFeatures { buildConfig true -<% if (project.moduleConfig === 'nitro-modules') { -%> +<% if (project.moduleConfig === 'nitro-modules' || project.viewConfig === 'nitro-view') { -%> prefab true <% } -%> } @@ -140,7 +140,7 @@ def kotlin_version = getExtOrDefault("kotlinVersion") dependencies { implementation "com.facebook.react:react-android" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" -<% if (project.moduleConfig === 'nitro-modules') { -%> +<% if (project.moduleConfig === 'nitro-modules' || project.viewConfig === 'nitro-view') { -%> implementation project(":react-native-nitro-modules") <% } -%> } diff --git a/packages/create-react-native-library/templates/native-common/{%- project.name %}.podspec b/packages/create-react-native-library/templates/native-common/{%- project.name %}.podspec index 17b3180b2..6a392b86b 100644 --- a/packages/create-react-native-library/templates/native-common/{%- project.name %}.podspec +++ b/packages/create-react-native-library/templates/native-common/{%- project.name %}.podspec @@ -13,7 +13,7 @@ Pod::Spec.new do |s| s.platforms = { :ios => min_ios_version_supported } s.source = { :git => "<%- repo -%>.git", :tag => "#{s.version}" } -<% if (project.moduleConfig !== "nitro-modules") { -%> +<% if (project.moduleConfig !== "nitro-modules" || project.viewConfig === "nitro-view") { -%> <% if (project.swift) { -%> s.source_files = "ios/**/*.{h,m,mm,swift}" <% } else { -%> @@ -22,7 +22,7 @@ Pod::Spec.new do |s| <% } -%> <% } -%> -<% if (project.moduleConfig === "nitro-modules") { -%> +<% if (project.moduleConfig === "nitro-modules" || project.viewConfig === "nitro-view") { -%> s.source_files = [ "ios/**/*.{swift}", "ios/**/*.{m,mm}", diff --git a/packages/create-react-native-library/templates/nitro-module/android/src/main/java/com/{%- project.package_dir %}/{%- project.name %}Package.kt b/packages/create-react-native-library/templates/nitro-module/android/src/main/java/com/{%- project.package_dir %}/{%- project.name %}Package.kt index f832910bc..4e2b8f2da 100644 --- a/packages/create-react-native-library/templates/nitro-module/android/src/main/java/com/{%- project.package_dir %}/{%- project.name %}Package.kt +++ b/packages/create-react-native-library/templates/nitro-module/android/src/main/java/com/{%- project.package_dir %}/{%- project.name %}Package.kt @@ -1,4 +1,4 @@ -package com.<%- project.package %> +package com.margelo.nitro.<%- project.package %> import com.facebook.react.TurboReactPackage import com.facebook.react.bridge.NativeModule diff --git a/packages/create-react-native-library/templates/nitro-view/android/CMakeLists.txt b/packages/create-react-native-library/templates/nitro-view/android/CMakeLists.txt new file mode 100644 index 000000000..1fa9ed39d --- /dev/null +++ b/packages/create-react-native-library/templates/nitro-view/android/CMakeLists.txt @@ -0,0 +1,24 @@ +project(<%- project.package_cpp -%>) +cmake_minimum_required(VERSION 3.9.0) + +set(PACKAGE_NAME <%- project.package_cpp -%>) +set(CMAKE_VERBOSE_MAKEFILE ON) +set(CMAKE_CXX_STANDARD 20) + +# Define C++ library and add all sources +add_library(${PACKAGE_NAME} SHARED src/main/cpp/cpp-adapter.cpp) + +# Add Nitrogen specs :) +include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/<%- project.package_cpp -%>+autolinking.cmake) + +# Set up local includes +include_directories("src/main/cpp" "../cpp") + +find_library(LOG_LIB log) + +# Link all libraries together +target_link_libraries( + ${PACKAGE_NAME} + ${LOG_LIB} + android # <-- Android core +) diff --git a/packages/create-react-native-library/templates/nitro-view/android/src/main/cpp/cpp-adapter.cpp b/packages/create-react-native-library/templates/nitro-view/android/src/main/cpp/cpp-adapter.cpp new file mode 100644 index 000000000..8e1f6123b --- /dev/null +++ b/packages/create-react-native-library/templates/nitro-view/android/src/main/cpp/cpp-adapter.cpp @@ -0,0 +1,6 @@ +#include +#include "<%- project.package_cpp -%>OnLoad.hpp" + +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) { + return margelo::nitro::<%- project.package_cpp -%>::initialize(vm); +} diff --git a/packages/create-react-native-library/templates/nitro-view/android/src/main/java/com/margelo/nitro/{%- project.package_dir %}/{%- project.name %}.kt b/packages/create-react-native-library/templates/nitro-view/android/src/main/java/com/margelo/nitro/{%- project.package_dir %}/{%- project.name %}.kt new file mode 100644 index 000000000..9634f8d57 --- /dev/null +++ b/packages/create-react-native-library/templates/nitro-view/android/src/main/java/com/margelo/nitro/{%- project.package_dir %}/{%- project.name %}.kt @@ -0,0 +1,23 @@ +package com.margelo.nitro.<%- project.package %> + +import android.view.View +import com.facebook.proguard.annotations.DoNotStrip +import com.facebook.react.uimanager.ThemedReactContext +import androidx.core.graphics.toColorInt + +@DoNotStrip +class Hybrid<%- project.name %>(val context: ThemedReactContext) : Hybrid<%- project.name %>Spec() { + + // View + override val view: View = View(context) + + // Props + private var _color = "#000" + override var color: String + get() = _color + set(value) { + _color = value + val color = value.toColorInt() + view.setBackgroundColor(color) + } +} diff --git a/packages/create-react-native-library/templates/nitro-view/android/src/main/java/com/{%- project.package_dir %}/{%- project.name %}Package.kt b/packages/create-react-native-library/templates/nitro-view/android/src/main/java/com/{%- project.package_dir %}/{%- project.name %}Package.kt new file mode 100644 index 000000000..c46a5ece2 --- /dev/null +++ b/packages/create-react-native-library/templates/nitro-view/android/src/main/java/com/{%- project.package_dir %}/{%- project.name %}Package.kt @@ -0,0 +1,28 @@ +package com.margelo.nitro.<%- project.package %> + +import com.facebook.react.TurboReactPackage +import com.facebook.react.bridge.NativeModule +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.module.model.ReactModuleInfoProvider +import com.facebook.react.uimanager.ViewManager +import com.margelo.nitro.<%- project.package_cpp -%>.views.Hybrid<%- project.name -%>Manager + +class <%- project.name -%>Package : TurboReactPackage() { + override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? { + return null + } + + override fun getReactModuleInfoProvider(): ReactModuleInfoProvider { + return ReactModuleInfoProvider { HashMap() } + } + + override fun createViewManagers(reactContext: ReactApplicationContext): List> { + return listOf(Hybrid<%- project.name -%>Manager()) + } + + companion object { + init { + System.loadLibrary("<%- project.package_cpp -%>") + } + } +} diff --git a/packages/create-react-native-library/templates/nitro-view/ios/{%- project.name %}.swift b/packages/create-react-native-library/templates/nitro-view/ios/{%- project.name %}.swift new file mode 100644 index 000000000..0e4c8ef3c --- /dev/null +++ b/packages/create-react-native-library/templates/nitro-view/ios/{%- project.name %}.swift @@ -0,0 +1,28 @@ +class Hybrid<%- project.name -%> : Hybrid<%- project.name -%>Spec { + + // UIView + var view: UIView = UIView() + + // props + var color: String = "#000" { + didSet { + view.backgroundColor = hexStringToUIColor(hexColor: color) + } + } + + func hexStringToUIColor(hexColor: String) -> UIColor { + let stringScanner = Scanner(string: hexColor) + + if(hexColor.hasPrefix("#")) { + stringScanner.scanLocation = 1 + } + var color: UInt32 = 0 + stringScanner.scanHexInt32(&color) + + let r = CGFloat(Int(color >> 16) & 0x000000FF) + let g = CGFloat(Int(color >> 8) & 0x000000FF) + let b = CGFloat(Int(color) & 0x000000FF) + + return UIColor(red: r / 255.0, green: g / 255.0, blue: b / 255.0, alpha: 1) + } +} diff --git a/packages/create-react-native-library/templates/nitro-view/nitro.json b/packages/create-react-native-library/templates/nitro-view/nitro.json new file mode 100644 index 000000000..78941a518 --- /dev/null +++ b/packages/create-react-native-library/templates/nitro-view/nitro.json @@ -0,0 +1,17 @@ +{ + "cxxNamespace": ["<%- project.package_cpp -%>"], + "ios": { + "iosModuleName": "<%- project.name -%>" + }, + "android": { + "androidNamespace": <%- JSON.stringify(project.package.split('.')) -%>, + "androidCxxLibName": "<%- project.package_cpp -%>" + }, + "autolinking": { + "<%- project.name -%>": { + "swift": "Hybrid<%- project.name -%>", + "kotlin": "Hybrid<%- project.name -%>" + } + }, + "ignorePaths": ["node_modules"] +} diff --git a/packages/create-react-native-library/templates/nitro-view/src/index.tsx b/packages/create-react-native-library/templates/nitro-view/src/index.tsx new file mode 100644 index 000000000..2f1c95268 --- /dev/null +++ b/packages/create-react-native-library/templates/nitro-view/src/index.tsx @@ -0,0 +1,11 @@ +import { getHostComponent } from 'react-native-nitro-modules'; +const <%- project.name %>Config = require('../nitrogen/generated/shared/json/<%- project.name %>Config.json'); +import type { + <%- project.name %>Methods, + <%- project.name %>Props, +} from './<%- project.name %>.nitro'; + +export const <%- project.name %>View = getHostComponent< + <%- project.name %>Props, + <%- project.name %>Methods +>('<%- project.name %>', () => <%- project.name %>Config); diff --git a/packages/create-react-native-library/templates/nitro-view/src/{%- project.name %}.nitro.ts b/packages/create-react-native-library/templates/nitro-view/src/{%- project.name %}.nitro.ts new file mode 100644 index 000000000..194477f98 --- /dev/null +++ b/packages/create-react-native-library/templates/nitro-view/src/{%- project.name %}.nitro.ts @@ -0,0 +1,15 @@ +import type { + HybridView, + HybridViewMethods, + HybridViewProps, +} from 'react-native-nitro-modules'; + +export interface <%- project.name %>Props extends HybridViewProps { + color: string; +} +export interface <%- project.name %>Methods extends HybridViewMethods {} + +export type <%- project.name %> = HybridView< + <%- project.name %>Props, + <%- project.name %>Methods +>;