Use the NativeScript runtime in a React Native app!
Still a work-in-progress. See the TODO section at the bottom of the Readme for current progress.
In short: We are not yet running NativeScript code in React Native. However, we do have a script to automatically integrate the NativeScript library set up in a React Native iOS project. I've not paid much attention to Android at all, yet.
npm install react-native-nativescript-runtime
import NativescriptRuntime from "react-native-nativescript-runtime";
// ...
const result = await NativescriptRuntime.multiply(3, 7);
I'll explain the repo structure.
This repo was initialised using the standard react-native-builder-bob tool, which bootstraps a best-practice boilerplate for making a React Native plugin (whilst also testing it on an example app).
To get an impression of what changes I've made beyond initialising the repo, see the next section where I provide a diff.
To understand what the purpose of this labyrinth of folders is, see my notes below.
Things to keep in mind:
- This React Native plugin, like most React Native plugins, will be distributed as an npm package that itself vends a locally-built CocoaPod and a locally-built Gradle file. Those locally-built native modules can make use of all the files that the npm package includes.
- The example app lives at
/example
- The iOS native sources live at
/ios
- The Android native sources live at
/android
- Any directory can be pulled into the npm package, e.g.
/scripts
, for use by the iOS and/or Android native modules. - Conversely, the iOS and/or Android native modules can choose to exclude some of the files in their directories (this is appropriate when we have e.g. an iOS-specific resource like
XCFrameworks.zip
that we want to simply unzip into the root of the app, rather than bundle into it as-is).
.
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── android # The React Native Android native module.
│ ├── build.gradle
│ └── src
│ └── main
│ ├── AndroidManifest.xml
│ └── java
│ └── com
│ └── reactnativenativescriptruntime
│ ├── NativescriptRuntimeModule.java
│ └── NativescriptRuntimePackage.java
├── babel.config.js # The Babel config for all TSX?/JSX? sources excluding `/example`, which has its own Babel config.
├── example # The folder for the example app. Unlike the convention in NativeScript, it is source-controlled.
│ ├── android # The Android example app. I haven't done much work on it yet.
│ │ ├── app
│ │ ├── build.gradle
│ │ ├── gradle
│ │ ├── gradle.properties
│ │ ├── gradlew
│ │ ├── gradlew.bat
│ │ ├── nativescript.build.gradle
│ │ ├── nativescript.buildscript.gradle
│ │ └── settings.gradle
│ ├── app.json # The React Native example app's metadata (display name, etc). I think this is an Expo concept.
│ ├── babel.config.js # The React Native example app's Babel config.
│ ├── index.tsx # The React Native example app's entrypoint.
│ ├── ios # The iOS example app.
│ │ ├── File.swift
│ │ ├── NativeScript # This is copied from the `ios` folder vended by the npm package.
│ │ ├── NativeScript.xcframework # This is unzipped from the `ios/XCFrameworks.zip` folder vended by the npm package.
│ │ ├── NativescriptRuntimeExample
│ │ ├── NativescriptRuntimeExample-Bridging-Header.h
│ │ ├── NativescriptRuntimeExample.xcodeproj
│ │ ├── NativescriptRuntimeExample.xcworkspace
│ │ ├── Podfile # The Podfile for the iOS app. Calls upon use_nativescript.rb.
│ │ ├── Podfile.lock
│ │ ├── Pods
│ │ ├── TNSWidgets.xcframework # This is unzipped from the `ios/XCFrameworks.zip` folder vended by the npm package.
│ │ ├── __MACOSX
│ │ ├── internal # This is copied from @nativescript/ios.
│ │ └── use_nativescript.rb # The script to install NativeScript. Once finalised, I plan to move this logic into:
| # /react-native-nativescript-runtime.podspec, or;
| # /scripts/use_nativescript.rb.
│ ├── metro.config.js # The bundler config for the React Native example app.
│ ├── nativescript
│ ├── node_modules
│ ├── package-lock.json
│ ├── package.json
│ └── src # The sources for the React Native example app.
| # I'm debating whether or not to nest the NativeScript sources in src/nativescript.
| # It's not clear what would be more intuitive in a React Native project.
├── ios # The React Native iOS native module.
│ ├── NativeScript # Copied from @nativescript/capacitor. Originally from @nativescript/ui-mobile-base.
│ │ ├── App-Bridging-Header.h
│ │ ├── Runtime.h
│ │ └── Runtime.m
│ ├── NativescriptRuntime.h # React Native iOS native module header. Not yet filled in.
│ ├── NativescriptRuntime.m # React Native iOS native module implementation. Not yet filled in.
│ ├── NativescriptRuntime.xcodeproj # React Native iOS native module xcodeproj.
│ └── XCFrameworks.zip # The JSC runtime, copied from @nativescript/capacitor.
# Unlike @nativescript/ios/framework/internal/XCFrameworks.zip found in @nativescript/ios@8.x.x,
# it lacks the TNSRuntime.h header (perhaps that's a JSC-only thing?).
# Oddly, in @nativescript/ios@6.5.4, there is no XCFrameworks.zip file at all!
# So it seems that this zip really was made bespoke for the Capacitor integration.
# It sounds like main reason we're using JSC rather than V8 is because this project is based on
# a camera project that nStudio did for a client in Ecuador.
# The good news is, this .zip is potentially stable indefinitely, as JSC is a slow-moving project.
├── package.json # When publishing this module to npm, it'll be from the root of the repo, using this.
├── react-native-nativescript-runtime.podspec # Specifies any native iOS files to be included in the CocoaPod.
├── scripts # Scripts to be used by both the npm package and (if necessary) the native modules.
│ ├── bootstrap.js
│ ├── build-nativescript.js
│ └── extractFrameworks.js
├── src # This is where you write the TypeScript API corresponding to your native module.
│ ├── __tests__
│ └── index.tsx
├── tsconfig.build.json # This examines all TypeScript sources excluding the example app, `/example`.
└── tsconfig.json
Here is a tree of changes I've made, relative to a freshly initialised react-native-builder-bob plugin. Note that this is very approximate, as things are changing rapidly from commit to commmit and I may simply miss some things. But it should at least give a general orientation as to what's specific to this plugin rather than just being typical React Native plugin boilerplate.
- In green are the files that I either committed to the repo as sources, or will be auto-provisioned upon building the project (e.g. due to the
use_nativescript.rb
iOS installer script, which is equivalent to thepostinstall.ts
script in the NativeScript Capacitor repo). - Marked with an asterisk are any files that I've modified (again, either committed to source or post-build).
.
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── android
│ ├── build.gradle *
│ └── src
│ └── main
├── babel.config.js
├── example
│ ├── android
│ │ ├── app
│ │ │ ├── build.gradle *
│ │ │ ├── debug.keystore
│ │ │ ├── proguard-rules.pro
│ │ │ └── src
│ │ │ └── main
│ │ │ └── AndroidManifest.xml *
│ │ ├── build.gradle *
│ │ ├── gradle
│ │ ├── gradle.properties
│ │ ├── gradlew
│ │ ├── gradlew.bat
+ │ │ ├── nativescript.build.gradle
+ │ │ ├── nativescript.buildscript.gradle
│ │ └── settings.gradle
│ ├── app.json
│ ├── babel.config.js
│ ├── index.tsx
│ ├── ios
│ │ ├── File.swift
+ │ │ ├── NativeScript
+ │ │ ├── NativeScript.xcframework
│ │ ├── NativescriptRuntimeExample
│ │ ├── NativescriptRuntimeExample-Bridging-Header.h
│ │ ├── NativescriptRuntimeExample.xcodeproj *
│ │ ├── NativescriptRuntimeExample.xcworkspace
│ │ ├── Podfile *
│ │ ├── Podfile.lock *
│ │ ├── Pods
+ │ │ ├── TNSWidgets.xcframework
+ │ │ ├── __MACOSX
+ │ │ ├── internal
+ │ │ └── use_nativescript.rb
│ ├── metro.config.js
+ │ ├── nativescript
+ │ │ ├── build
+ │ │ ├── package.json
+ │ │ ├── references.d.ts
+ │ │ ├── src
+ │ │ └── tsconfig.json
│ ├── node_modules
│ ├── package-lock.json *
│ ├── package.json *
│ └── src
│ └── App.tsx
├── ios
+ │ ├── NativeScript
+ │ │ ├── App-Bridging-Header.h
+ │ │ ├── Runtime.h
+ │ │ └── Runtime.m
│ ├── NativescriptRuntime.h
│ ├── NativescriptRuntime.m
│ ├── NativescriptRuntime.xcodeproj
│ │ ├── project.pbxproj
│ │ └── project.xcworkspace
+ │ └── XCFrameworks.zip
├── package.json
├── react-native-nativescript-runtime.podspec
├── scripts
│ ├── bootstrap.js
+ │ └── build-nativescript.js
├── src
│ ├── __tests__
│ │ └── index.test.tsx
│ └── index.tsx
├── tsconfig.build.json
└── tsconfig.json
Nothing working end-to-end yet; very much under construction.
- Assemble everything from the Capacitor
embed
folder. Reasonably confident that I've integrated the build scripts correctly, but not so sure about all the other stuff. - Do a test-run and see if both the app and native module compile at all.
- Check whether all the file paths and assets used by the native module resolve correctly. I see many usages of
$rootDir
,$userDir/nsconfig.json
, etc., which may all make incorrect assumptions. - See whether any of the Android files can be derived from
@nativescript/android
instead. - Write the bridge code.
- Convert to a TurboModule.
Auto-provisioning of the Xcode project is complete. Some steps remain manual for now.
- [Swift support] Allow (or simply instruct) users how to use their own bridging header
- [Swift support] Support
AppDelegate.swift
- Support auto-injection of runtime support into
AppDelegate.{m,swift}
(see the code blocks surrounded by// START NativeScript runtime
and// END NativeScript runtime
), or just give instructions. - Support uninstallation of NativeScript runtime support.
- Move
use_nativescript.rb
up from the app to the iOS native module. It's only really there at the moment so that I have a shorter path to type out when running it on the CLI. - Take
TNSWidgets
from@nativescript/core
rather than committing it to this repo. -
Check whetherI checkedNativeScript.xcframework
can be taken from anywhere.@nativescript/ios
, both v6.x.x (JSC) and v8.x.x (V8), but it wasn't there. Seems it's bespoke to the Capacitor effort. - Convert to a TurboModule.
Despite bundling XCFrameworks.zip
, we do still need to ensure that the root project's node_modules
holds @nativescript/ios
(whether as a direct dependency or as a subdependency of our npm package), because that's where our Cocoapod pulls the whole @nativescript/ios/framework/internal
folder from (which provides things like the nativescript-pre-build
script).
There's an argument that maybe we should just bundle the internal
folder into the native module as we're already doing so for XCFrameworks.zip
, but I prefer to re-use published packages wherever possible. Ideally at some point we'd be able to just cleanly source everything from @nativescript/ios
.
It's probably best for it to be the responsibility of the app itself to install @nativescript/ios
and/or @nativescript/android
, to save disk space for anyone working on a single-platform project.
Figure out how to make NativeScript bundle (see scripts/build-nativescript.js
).
Build an API for sending messages to the NativeScript runtime from the React Native JS context. i.e. make a React Native native module under /ios
and /android
, with reference to the Capacitor one.
-
I initialised a React Native plugin using react-native-builder-bob, selecting "Java & Objective-C" as the library type:
npx create-react-native-library react-native-nativescript-runtime
-
I ported the NativeScript Capacitor postinstall script to Ruby (see
example/ios/use_nativescript.rb
), adjusting it for the structure of a React Native app: https://github.com/NativeScript/capacitor/blob/main/src/postinstall.ts
See the contributing guide to learn how to contribute to the repository and the development workflow.
MIT