From f44d92eb7938a98b0ef77def0a8dacbf06da0bc8 Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Sat, 24 Oct 2026 10:11:05 -0700 Subject: [PATCH 1/2] fix(android): warn when a Java package global falls back to $name A host can own a top-level package name as a read-only global; Expo defines `expo`. The jsi runtime already exposed such a package as `$expo`, but did so silently. It now logs a warning naming both globals. The napi runtime had no fallback: depending on the engine, the assignment either failed or was ignored, and the package was not reachable at all. It now gets the same `$name` fallback and warning. --- .../ffi/jni/jsi/metadata/MetadataNode.cpp | 11 +++++-- .../ffi/jni/napi/metadata/MetadataNode.cpp | 32 ++++++++++++++++++- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/NativeScript/ffi/jni/jsi/metadata/MetadataNode.cpp b/NativeScript/ffi/jni/jsi/metadata/MetadataNode.cpp index 6187a585..fd15727c 100644 --- a/NativeScript/ffi/jni/jsi/metadata/MetadataNode.cpp +++ b/NativeScript/ffi/jni/jsi/metadata/MetadataNode.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include "NativeScriptException.h" #include "MetadataNode.h" #include "CallbackHandlers.h" @@ -890,9 +891,13 @@ void MetadataNode::CreateTopLevelNamespaces(JsRuntime &rt) { try { global.setProperty(rt, nameSpace.c_str(), packageObj); } catch (JsError &) { - // A host can already own the name as a non-writable global (Expo - // defines `expo`), so fall back to the same $ prefix as keywords. - global.setProperty(rt, ("$" + nameSpace).c_str(), packageObj); + // The host already owns the name as a read-only global (Expo's + // `expo`), so the package takes the same $ prefix as keywords. + string fallback = "$" + nameSpace; + __android_log_print(ANDROID_LOG_WARN, "TNS.Runtime", + "Java package '%s' is exposed as '%s': the global '%s' is read-only.", + nameSpace.c_str(), fallback.c_str(), nameSpace.c_str()); + global.setProperty(rt, fallback.c_str(), packageObj); } } } diff --git a/NativeScript/ffi/jni/napi/metadata/MetadataNode.cpp b/NativeScript/ffi/jni/napi/metadata/MetadataNode.cpp index 6e9e4821..21dbbc6d 100644 --- a/NativeScript/ffi/jni/napi/metadata/MetadataNode.cpp +++ b/NativeScript/ffi/jni/napi/metadata/MetadataNode.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include "NativeScriptException.h" #include "MetadataNode.h" #include "CallbackHandlers.h" @@ -1095,6 +1096,27 @@ MetadataNode::MetadataNode(MetadataTreeNode *treeNode) : m_treeNode(treeNode) { } } +// Engines differ on assigning to a read-only property: some fail the call, +// others ignore the write. Either way the global does not end up holding value. +static bool SetGlobalIfWritable(napi_env env, napi_value global, const string &name, + napi_value value) { + if (napi_set_named_property(env, global, name.c_str(), value) == napi_ok) { + napi_value assigned; + bool same = false; + if (napi_get_named_property(env, global, name.c_str(), &assigned) == napi_ok && + napi_strict_equals(env, assigned, value, &same) == napi_ok && same) { + return true; + } + } + bool pending = false; + napi_is_exception_pending(env, &pending); + if (pending) { + napi_value error; + napi_get_and_clear_last_exception(env, &error); + } + return false; +} + void MetadataNode::CreateTopLevelNamespaces(napi_env env) { napi_status status; napi_value global; @@ -1120,7 +1142,15 @@ void MetadataNode::CreateTopLevelNamespaces(napi_env env) { if (IsJavascriptKeyword(nameSpace)) { nameSpace = "$" + nameSpace; } - NAPI_GUARD(napi_set_named_property(env, global, nameSpace.c_str(), packageObj)) {} + if (!SetGlobalIfWritable(env, global, nameSpace, packageObj)) { + // The host already owns the name as a read-only global (Expo's + // `expo`), so the package takes the same $ prefix as keywords. + string fallback = "$" + nameSpace; + __android_log_print(ANDROID_LOG_WARN, "TNS.Runtime", + "Java package '%s' is exposed as '%s': the global '%s' is read-only.", + nameSpace.c_str(), fallback.c_str(), nameSpace.c_str()); + NAPI_GUARD(napi_set_named_property(env, global, fallback.c_str(), packageObj)) {} + } } } } From 0948f32013d605bbabaaedc819a8503ab76fbc58 Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Sun, 25 Oct 2026 13:21:48 -0700 Subject: [PATCH 2/2] fix(android): use $name when a Java package's global name is taken Check whether the global already has the package's name and, if it does, expose the package as `$name`, logging a warning. Both the jsi and napi trees now do the same check, so an existing global is never overwritten and no engine-specific read-only handling is needed. --- .../ffi/jni/jsi/metadata/MetadataNode.cpp | 11 +++---- .../ffi/jni/napi/metadata/MetadataNode.cpp | 31 +++---------------- 2 files changed, 9 insertions(+), 33 deletions(-) diff --git a/NativeScript/ffi/jni/jsi/metadata/MetadataNode.cpp b/NativeScript/ffi/jni/jsi/metadata/MetadataNode.cpp index fd15727c..65193022 100644 --- a/NativeScript/ffi/jni/jsi/metadata/MetadataNode.cpp +++ b/NativeScript/ffi/jni/jsi/metadata/MetadataNode.cpp @@ -888,17 +888,14 @@ void MetadataNode::CreateTopLevelNamespaces(JsRuntime &rt) { if (IsJavascriptKeyword(nameSpace)) { nameSpace = "$" + nameSpace; } - try { - global.setProperty(rt, nameSpace.c_str(), packageObj); - } catch (JsError &) { - // The host already owns the name as a read-only global (Expo's - // `expo`), so the package takes the same $ prefix as keywords. + if (global.hasProperty(rt, nameSpace.c_str())) { string fallback = "$" + nameSpace; __android_log_print(ANDROID_LOG_WARN, "TNS.Runtime", - "Java package '%s' is exposed as '%s': the global '%s' is read-only.", + "Java package '%s' is exposed as '%s': the global '%s' already exists.", nameSpace.c_str(), fallback.c_str(), nameSpace.c_str()); - global.setProperty(rt, fallback.c_str(), packageObj); + nameSpace = fallback; } + global.setProperty(rt, nameSpace.c_str(), packageObj); } } } diff --git a/NativeScript/ffi/jni/napi/metadata/MetadataNode.cpp b/NativeScript/ffi/jni/napi/metadata/MetadataNode.cpp index 21dbbc6d..af866da3 100644 --- a/NativeScript/ffi/jni/napi/metadata/MetadataNode.cpp +++ b/NativeScript/ffi/jni/napi/metadata/MetadataNode.cpp @@ -1096,27 +1096,6 @@ MetadataNode::MetadataNode(MetadataTreeNode *treeNode) : m_treeNode(treeNode) { } } -// Engines differ on assigning to a read-only property: some fail the call, -// others ignore the write. Either way the global does not end up holding value. -static bool SetGlobalIfWritable(napi_env env, napi_value global, const string &name, - napi_value value) { - if (napi_set_named_property(env, global, name.c_str(), value) == napi_ok) { - napi_value assigned; - bool same = false; - if (napi_get_named_property(env, global, name.c_str(), &assigned) == napi_ok && - napi_strict_equals(env, assigned, value, &same) == napi_ok && same) { - return true; - } - } - bool pending = false; - napi_is_exception_pending(env, &pending); - if (pending) { - napi_value error; - napi_get_and_clear_last_exception(env, &error); - } - return false; -} - void MetadataNode::CreateTopLevelNamespaces(napi_env env) { napi_status status; napi_value global; @@ -1142,15 +1121,15 @@ void MetadataNode::CreateTopLevelNamespaces(napi_env env) { if (IsJavascriptKeyword(nameSpace)) { nameSpace = "$" + nameSpace; } - if (!SetGlobalIfWritable(env, global, nameSpace, packageObj)) { - // The host already owns the name as a read-only global (Expo's - // `expo`), so the package takes the same $ prefix as keywords. + bool exists = false; + if (napi_has_named_property(env, global, nameSpace.c_str(), &exists) == napi_ok && exists) { string fallback = "$" + nameSpace; __android_log_print(ANDROID_LOG_WARN, "TNS.Runtime", - "Java package '%s' is exposed as '%s': the global '%s' is read-only.", + "Java package '%s' is exposed as '%s': the global '%s' already exists.", nameSpace.c_str(), fallback.c_str(), nameSpace.c_str()); - NAPI_GUARD(napi_set_named_property(env, global, fallback.c_str(), packageObj)) {} + nameSpace = fallback; } + NAPI_GUARD(napi_set_named_property(env, global, nameSpace.c_str(), packageObj)) {} } } }