From 31932e91e377944a5bb514654f0ab4a61712325f Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Sat, 24 Oct 2026 10:09:26 -0700 Subject: [PATCH 1/2] fix(react-native): import without the native module; init() on Android - The TurboModule is looked up with TurboModuleRegistry.get, so importing the package in Expo Go or a build made before it was added no longer raises an uncatchable invariant. isAvailable() reports whether the module is present; init() and the other functions that need it throw an error saying to rebuild. - init() no longer installs into a Worklets runtime on Android, where there is none, so it returns true instead of throwing a message about missing react-native-worklets setup. runOnUI()/installWorklets() on Android say they are iOS only. --- .../react-native/src/NativeScriptNativeApi.ts | 5 +- packages/react-native/src/index.d.ts | 7 +++ packages/react-native/src/index.ts | 60 +++++++++++++++---- 3 files changed, 59 insertions(+), 13 deletions(-) diff --git a/packages/react-native/src/NativeScriptNativeApi.ts b/packages/react-native/src/NativeScriptNativeApi.ts index 4cb54f02e..ca8da1259 100644 --- a/packages/react-native/src/NativeScriptNativeApi.ts +++ b/packages/react-native/src/NativeScriptNativeApi.ts @@ -14,4 +14,7 @@ export interface Spec extends TurboModule { readonly __writeTestMarker: (content: string) => boolean; } -export default TurboModuleRegistry.getEnforcing('NativeScriptNativeApi'); +// `get`, not `getEnforcing`: a build without the native module (Expo Go, or a +// build made before the package was added) must still be able to import the +// package and decide what to do without it. +export default TurboModuleRegistry.get('NativeScriptNativeApi'); diff --git a/packages/react-native/src/index.d.ts b/packages/react-native/src/index.d.ts index 3e15e684d..1703d9dd3 100644 --- a/packages/react-native/src/index.d.ts +++ b/packages/react-native/src/index.d.ts @@ -273,6 +273,12 @@ export type UIViewControllerDefinition< export function init(metadataPath?: string, options?: InstallOptions): boolean; export const install: typeof init; export function installGlobals(): boolean; +/** + * Whether this build contains the package's native module. False in Expo Go + * and in builds made before the package was added; every other function that + * needs the module throws there. + */ +export function isAvailable(): boolean; export function isInstalled(): boolean; export function defaultMetadataPath(): string; export function getRuntimeBackend(): string; @@ -342,6 +348,7 @@ declare const NativeScript: { init: typeof init; install: typeof install; installGlobals: typeof installGlobals; + isAvailable: typeof isAvailable; isInstalled: typeof isInstalled; defaultMetadataPath: typeof defaultMetadataPath; defineUIKitContainer: typeof defineUIKitContainer; diff --git a/packages/react-native/src/index.ts b/packages/react-native/src/index.ts index b15d01120..36652aea8 100644 --- a/packages/react-native/src/index.ts +++ b/packages/react-native/src/index.ts @@ -12,7 +12,9 @@ import type { RefAttributes, } from "react"; import type { ViewProps } from "react-native"; +import { Platform } from "react-native"; import NativeScriptNativeApi from "./NativeScriptNativeApi"; +import type { Spec as NativeScriptNativeApiSpec } from "./NativeScriptNativeApi"; import NativeScriptUIViewNativeComponent from "./NativeScriptUIViewNativeComponent"; declare const require: (id: string) => any; @@ -983,10 +985,38 @@ export function installGlobals(): boolean { return true; } +function nativeModule(): NativeScriptNativeApiSpec { + if (NativeScriptNativeApi == null) { + throw new Error( + "@nativescript/react-native's native module (NativeScriptNativeApi) is not in this build. " + + "The package contains native code, so Expo Go cannot load it: install it in a development " + + "build and rebuild the app (npx expo run:ios / run:android, or EAS Build).", + ); + } + return NativeScriptNativeApi; +} + +// Android has no Worklets runtime to install into (see installWorkletRuntime in +// NativeScriptNativeApiModule.java). +function supportsWorkletRuntime(): boolean { + return Platform.OS === "ios"; +} + +function assertWorkletRuntimeSupported(): void { + if (!supportsWorkletRuntime()) { + throw new Error( + `NativeScript does not support Worklets on ${Platform.OS}: there is no UI runtime to install into, so runOnUI and UIKit views are iOS only.`, + ); + } +} + +export function isAvailable(): boolean { + return NativeScriptNativeApi != null; +} + export function init(metadataPath = "", options: InstallOptions = {}): boolean { - const installed = - NativeScriptNativeApi.isInstalled() || - NativeScriptNativeApi.install(metadataPath); + const api = nativeModule(); + const installed = api.isInstalled() || api.install(metadataPath); if (installed) { installInteropConstructors(); installInlineFunctions(); @@ -994,7 +1024,7 @@ export function init(metadataPath = "", options: InstallOptions = {}): boolean { if (installed && options.globals === true) { installGlobals(); } - if (installed) { + if (installed && supportsWorkletRuntime()) { ensureWorkletsInstalled(metadataPath); } return installed; @@ -1003,15 +1033,15 @@ export function init(metadataPath = "", options: InstallOptions = {}): boolean { export const install = init; export function isInstalled(): boolean { - return NativeScriptNativeApi.isInstalled(); + return NativeScriptNativeApi != null && NativeScriptNativeApi.isInstalled(); } export function defaultMetadataPath(): string { - return NativeScriptNativeApi.defaultMetadataPath(); + return nativeModule().defaultMetadataPath(); } export function getRuntimeBackend(): string { - return NativeScriptNativeApi.getRuntimeBackend(); + return nativeModule().getRuntimeBackend(); } let workletsAdapter: NativeScriptWorklets | undefined; @@ -1292,16 +1322,19 @@ function ensureWorkletsInstalled(metadataPath = ""): NativeScriptWorklets { if (workletsAdapter) { return workletsAdapter; } + assertWorkletRuntimeSupported(); installWorklets(requireReactNativeWorklets(), metadataPath); return workletsAdapter as NativeScriptWorklets; } export function installWorklets( - worklets: NativeScriptWorklets = requireReactNativeWorklets(), + worklets?: NativeScriptWorklets, metadataPath = "", ): boolean { - if (!NativeScriptNativeApi.isInstalled()) { - const installed = NativeScriptNativeApi.install(metadataPath); + const api = nativeModule(); + assertWorkletRuntimeSupported(); + if (!api.isInstalled()) { + const installed = api.install(metadataPath); if (!installed) { throw new Error( "NativeScript Native API JSI host object was not installed", @@ -1311,14 +1344,16 @@ export function installWorklets( installInlineFunctions(); } - const validWorklets = validateWorkletsModule(worklets); + const validWorklets = validateWorkletsModule( + worklets ?? requireReactNativeWorklets(), + ); const holder = validWorklets.getUIRuntimeHolder(); if (holder == null || typeof holder !== "object") { throw workletsSetupError( "NativeScript.runOnUI could not resolve a Worklets UI runtime", ); } - const installRuntime = NativeScriptNativeApi.installWorkletRuntime; + const installRuntime = api.installWorkletRuntime; if (typeof installRuntime !== "function") { throw workletsSetupError( "NativeScript Native API was built without RNWorklets runtime support", @@ -3234,6 +3269,7 @@ const NativeScript = { init, install, installGlobals, + isAvailable, isInstalled, defaultMetadataPath, defineUIKitContainer, From 31cfa36580fa18ec88f6dd1c6ad8ab708cdea3c7 Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Sun, 25 Oct 2026 13:21:48 -0700 Subject: [PATCH 2/2] fix(react-native): explain a missing native module at import Keep failing at import when NativeScriptNativeApi is not in the app binary, but with an error that says the package cannot run in Expo Go and that a build made before the package was added must be rebuilt, instead of React Native's generic TurboModuleRegistry invariant. Drop isAvailable() and the Android-specific init() path. --- .../react-native/src/NativeScriptNativeApi.ts | 18 ++++-- packages/react-native/src/index.d.ts | 7 --- packages/react-native/src/index.ts | 60 ++++--------------- 3 files changed, 26 insertions(+), 59 deletions(-) diff --git a/packages/react-native/src/NativeScriptNativeApi.ts b/packages/react-native/src/NativeScriptNativeApi.ts index ca8da1259..e611d7319 100644 --- a/packages/react-native/src/NativeScriptNativeApi.ts +++ b/packages/react-native/src/NativeScriptNativeApi.ts @@ -14,7 +14,17 @@ export interface Spec extends TurboModule { readonly __writeTestMarker: (content: string) => boolean; } -// `get`, not `getEnforcing`: a build without the native module (Expo Go, or a -// build made before the package was added) must still be able to import the -// package and decide what to do without it. -export default TurboModuleRegistry.get('NativeScriptNativeApi'); +function loadNativeApi(): Spec { + const nativeApi = TurboModuleRegistry.get('NativeScriptNativeApi'); + if (nativeApi == null) { + throw new Error( + "@nativescript/react-native: the native module 'NativeScriptNativeApi' is not in this app binary. " + + 'The package contains native code, so it cannot run in Expo Go: use a development build ' + + '(npx expo run:ios / npx expo run:android, or EAS Build). ' + + 'If the package was added after the app was built, rebuild the app.', + ); + } + return nativeApi; +} + +export default loadNativeApi(); diff --git a/packages/react-native/src/index.d.ts b/packages/react-native/src/index.d.ts index 1703d9dd3..3e15e684d 100644 --- a/packages/react-native/src/index.d.ts +++ b/packages/react-native/src/index.d.ts @@ -273,12 +273,6 @@ export type UIViewControllerDefinition< export function init(metadataPath?: string, options?: InstallOptions): boolean; export const install: typeof init; export function installGlobals(): boolean; -/** - * Whether this build contains the package's native module. False in Expo Go - * and in builds made before the package was added; every other function that - * needs the module throws there. - */ -export function isAvailable(): boolean; export function isInstalled(): boolean; export function defaultMetadataPath(): string; export function getRuntimeBackend(): string; @@ -348,7 +342,6 @@ declare const NativeScript: { init: typeof init; install: typeof install; installGlobals: typeof installGlobals; - isAvailable: typeof isAvailable; isInstalled: typeof isInstalled; defaultMetadataPath: typeof defaultMetadataPath; defineUIKitContainer: typeof defineUIKitContainer; diff --git a/packages/react-native/src/index.ts b/packages/react-native/src/index.ts index 36652aea8..b15d01120 100644 --- a/packages/react-native/src/index.ts +++ b/packages/react-native/src/index.ts @@ -12,9 +12,7 @@ import type { RefAttributes, } from "react"; import type { ViewProps } from "react-native"; -import { Platform } from "react-native"; import NativeScriptNativeApi from "./NativeScriptNativeApi"; -import type { Spec as NativeScriptNativeApiSpec } from "./NativeScriptNativeApi"; import NativeScriptUIViewNativeComponent from "./NativeScriptUIViewNativeComponent"; declare const require: (id: string) => any; @@ -985,38 +983,10 @@ export function installGlobals(): boolean { return true; } -function nativeModule(): NativeScriptNativeApiSpec { - if (NativeScriptNativeApi == null) { - throw new Error( - "@nativescript/react-native's native module (NativeScriptNativeApi) is not in this build. " + - "The package contains native code, so Expo Go cannot load it: install it in a development " + - "build and rebuild the app (npx expo run:ios / run:android, or EAS Build).", - ); - } - return NativeScriptNativeApi; -} - -// Android has no Worklets runtime to install into (see installWorkletRuntime in -// NativeScriptNativeApiModule.java). -function supportsWorkletRuntime(): boolean { - return Platform.OS === "ios"; -} - -function assertWorkletRuntimeSupported(): void { - if (!supportsWorkletRuntime()) { - throw new Error( - `NativeScript does not support Worklets on ${Platform.OS}: there is no UI runtime to install into, so runOnUI and UIKit views are iOS only.`, - ); - } -} - -export function isAvailable(): boolean { - return NativeScriptNativeApi != null; -} - export function init(metadataPath = "", options: InstallOptions = {}): boolean { - const api = nativeModule(); - const installed = api.isInstalled() || api.install(metadataPath); + const installed = + NativeScriptNativeApi.isInstalled() || + NativeScriptNativeApi.install(metadataPath); if (installed) { installInteropConstructors(); installInlineFunctions(); @@ -1024,7 +994,7 @@ export function init(metadataPath = "", options: InstallOptions = {}): boolean { if (installed && options.globals === true) { installGlobals(); } - if (installed && supportsWorkletRuntime()) { + if (installed) { ensureWorkletsInstalled(metadataPath); } return installed; @@ -1033,15 +1003,15 @@ export function init(metadataPath = "", options: InstallOptions = {}): boolean { export const install = init; export function isInstalled(): boolean { - return NativeScriptNativeApi != null && NativeScriptNativeApi.isInstalled(); + return NativeScriptNativeApi.isInstalled(); } export function defaultMetadataPath(): string { - return nativeModule().defaultMetadataPath(); + return NativeScriptNativeApi.defaultMetadataPath(); } export function getRuntimeBackend(): string { - return nativeModule().getRuntimeBackend(); + return NativeScriptNativeApi.getRuntimeBackend(); } let workletsAdapter: NativeScriptWorklets | undefined; @@ -1322,19 +1292,16 @@ function ensureWorkletsInstalled(metadataPath = ""): NativeScriptWorklets { if (workletsAdapter) { return workletsAdapter; } - assertWorkletRuntimeSupported(); installWorklets(requireReactNativeWorklets(), metadataPath); return workletsAdapter as NativeScriptWorklets; } export function installWorklets( - worklets?: NativeScriptWorklets, + worklets: NativeScriptWorklets = requireReactNativeWorklets(), metadataPath = "", ): boolean { - const api = nativeModule(); - assertWorkletRuntimeSupported(); - if (!api.isInstalled()) { - const installed = api.install(metadataPath); + if (!NativeScriptNativeApi.isInstalled()) { + const installed = NativeScriptNativeApi.install(metadataPath); if (!installed) { throw new Error( "NativeScript Native API JSI host object was not installed", @@ -1344,16 +1311,14 @@ export function installWorklets( installInlineFunctions(); } - const validWorklets = validateWorkletsModule( - worklets ?? requireReactNativeWorklets(), - ); + const validWorklets = validateWorkletsModule(worklets); const holder = validWorklets.getUIRuntimeHolder(); if (holder == null || typeof holder !== "object") { throw workletsSetupError( "NativeScript.runOnUI could not resolve a Worklets UI runtime", ); } - const installRuntime = api.installWorkletRuntime; + const installRuntime = NativeScriptNativeApi.installWorkletRuntime; if (typeof installRuntime !== "function") { throw workletsSetupError( "NativeScript Native API was built without RNWorklets runtime support", @@ -3269,7 +3234,6 @@ const NativeScript = { init, install, installGlobals, - isAvailable, isInstalled, defaultMetadataPath, defineUIKitContainer,