Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
3fd2932
feat: add React Native UIKit runtime primitives
DjDeveloperr Jun 29, 2026
831d34a
docs: record RN module PR packaging
DjDeveloperr Jun 29, 2026
096b2b8
fix: preserve dynamic native class identity
DjDeveloperr Jun 29, 2026
bd296cd
fix: cache write-only runtime property values
DjDeveloperr Jun 29, 2026
c0acd15
fix: cache unreadable runtime setter values
DjDeveloperr Jun 29, 2026
2b5ec0a
fix: persist UIAppearance proxy setter values
DjDeveloperr Jun 29, 2026
049f9af
fix: guard UIAppearance proxy detection
DjDeveloperr Jun 29, 2026
473788d
fix: tag UIAppearance proxy results
DjDeveloperr Jun 29, 2026
3ad3920
fix: identify UIAppearance proxy fallbacks safely
DjDeveloperr Jun 29, 2026
45c6df6
fix: tag static UIAppearance selector results
DjDeveloperr Jun 29, 2026
cb4fc1a
fix: cache metadata UIAppearance setters
DjDeveloperr Jun 29, 2026
9699fef
fix: cache protocol UIAppearance accessors
DjDeveloperr Jun 29, 2026
c26766e
fix: cache selector group UIAppearance accessors
DjDeveloperr Jun 29, 2026
94df696
fix: dispatch UIAppearance proxy setters via target metadata
DjDeveloperr Jun 29, 2026
2c6bce7
fix: detect UIAppearance targets from exact descriptions
DjDeveloperr Jun 29, 2026
c7454bc
fix: install UIAppearance target prototypes
DjDeveloperr Jun 29, 2026
ac94954
fix: avoid UIAppearance proxy getter hangs
DjDeveloperr Jun 29, 2026
58e5522
fix: limit appearance proxy accessors to UIKit
DjDeveloperr Jun 29, 2026
14caab6
fix: cache appearance values from runtime setters
DjDeveloperr Jun 29, 2026
1b12a71
fix: cache appearance values from host sets
DjDeveloperr Jun 29, 2026
7eae675
fix: prefer writable appearance proxy members
DjDeveloperr Jun 29, 2026
99889cb
fix: stabilize object expando runtime keys
DjDeveloperr Jun 29, 2026
72f1af0
test: expose iOS simulator hangs
DjDeveloperr Jun 29, 2026
d4cae97
fix: bridge indexed collection subclass aliases
DjDeveloperr Jun 29, 2026
197577e
test: trace NSMutableArray native callback
DjDeveloperr Jun 29, 2026
289e52c
test: trace NSMutableArray JS dispatch
DjDeveloperr Jun 29, 2026
c201b4c
fix: dispatch instance base selectors through super
DjDeveloperr Jun 30, 2026
20dbc6b
fix: preserve receiver identity for base initializers
DjDeveloperr Jun 30, 2026
b016ae8
fix: preserve initializer receiver identity
DjDeveloperr Jun 30, 2026
7d7e9d3
fix: persist JS subclass instance fields
DjDeveloperr Jun 30, 2026
1723f06
fix: enumerate JS indexed collection subclasses
DjDeveloperr Jun 30, 2026
7242ea9
test: trace NSMutableArray iOS hang
DjDeveloperr Jun 30, 2026
bd5c264
fix: preserve initializer bridge state
DjDeveloperr Jun 30, 2026
ca53096
fix: construct JS subclasses through remembered init
DjDeveloperr Jun 30, 2026
59a756f
fix: preserve init receiver native identity
DjDeveloperr Jun 30, 2026
3b7a492
fix: guard JS callbacks during subclass init
DjDeveloperr Jun 30, 2026
2295794
fix: prefer JS subclass property accessors
DjDeveloperr Jun 30, 2026
241c994
fix: prefer JS subclass property setters
DjDeveloperr Jun 30, 2026
353e727
fix: invoke JS subclass property setters
DjDeveloperr Jun 30, 2026
3087119
fix: resolve JS subclass registered prototypes
DjDeveloperr Jun 30, 2026
dc038a4
fix: honor V8 native instance prototype accessors
DjDeveloperr Jun 30, 2026
3743381
fix: suppress JS accessor callback reentry
DjDeveloperr Jun 30, 2026
2730d71
docs: update RNS parity progress
DjDeveloperr Jun 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,6 @@ jobs:
IOS_TEST_TIMEOUT_MS: "600000"
IOS_TEST_INACTIVITY_TIMEOUT_MS: "180000"
IOS_LOG_JUNIT: "1"
IOS_TEST_VERBOSE_SPECS: "1"
IOS_SIMCTL_QUERY_TIMEOUT_MS: "10000"
run: npm run test:ios
1,876 changes: 1,876 additions & 0 deletions HANDOFF.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions NativeScript/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,7 @@ if(ENABLE_JS_RUNTIME)
runtime/modules/app/App.mm
runtime/modules/web/Web.mm
runtime/NativeScript.mm
cli/BundleLoader.mm
runtime/RuntimeConfig.cpp
runtime/modules/url/ada/ada.cpp
runtime/modules/url/URL.cpp
Expand Down
104 changes: 101 additions & 3 deletions NativeScript/cli/BundleLoader.mm
Original file line number Diff line number Diff line change
@@ -1,50 +1,148 @@
#include "BundleLoader.h"
#include <Foundation/Foundation.h>
#include <mach-o/dyld.h>
#include <stdlib.h>

// Check if Resources/app/ exists, then load package.json["main"] || app/index.js full file path

std::string resolveMainPath() {
static NSString* resourcesPathForExecutable(NSString* executablePath) {
if (executablePath == nil || [executablePath length] == 0) {
return nil;
}

NSString* standardizedPath = [executablePath stringByStandardizingPath];
NSString* macOSPath = [standardizedPath stringByDeletingLastPathComponent];
NSString* contentsPath = [macOSPath stringByDeletingLastPathComponent];
if ([[macOSPath lastPathComponent] isEqualToString:@"MacOS"] &&
[[contentsPath lastPathComponent] isEqualToString:@"Contents"]) {
return [contentsPath stringByAppendingPathComponent:@"Resources"];
}

return nil;
}

static bool shouldLogBundleResolution() {
return getenv("NS_BUNDLE_LOADER_DEBUG") != nullptr;
}

static void addCandidatePath(NSMutableArray<NSString*>* candidates, NSString* path) {
if (path == nil || [path length] == 0) {
return;
}

NSString* standardizedPath = [path stringByStandardizingPath];
if (![candidates containsObject:standardizedPath]) {
[candidates addObject:standardizedPath];
}
}

static std::string resolveMainPathInResources(NSString* resourcesPath) {
NSFileManager* fileManager = [NSFileManager defaultManager];
NSString* resourcesPath = [[NSBundle mainBundle] resourcePath];
NSString* appPath = [resourcesPath stringByAppendingPathComponent:@"app"];
BOOL isDir;

if ([fileManager fileExistsAtPath:appPath isDirectory:&isDir] && isDir) {
if (shouldLogBundleResolution()) {
NSLog(@"NativeScript BundleLoader checking app path: %@", appPath);
}
NSString* packageJsonPath = [appPath stringByAppendingPathComponent:@"package.json"];
if ([fileManager fileExistsAtPath:packageJsonPath]) {
NSData* jsonData = [NSData dataWithContentsOfFile:packageJsonPath];
NSError* error;
NSError* error = nil;
NSDictionary* packageDict = [NSJSONSerialization JSONObjectWithData:jsonData
options:0
error:&error];
if (error == nil) {
NSString* mainEntry = packageDict[@"main"];
if (shouldLogBundleResolution()) {
NSLog(@"NativeScript BundleLoader package main: %@ from %@", mainEntry, packageJsonPath);
}
if (mainEntry != nil) {
NSString* mainPath = [appPath stringByAppendingPathComponent:mainEntry];
if ([fileManager fileExistsAtPath:mainPath]) {
if (shouldLogBundleResolution()) {
NSLog(@"NativeScript BundleLoader resolved main: %@", mainPath);
}
return std::string([mainPath UTF8String]);
}

if ([[mainEntry pathExtension] length] == 0) {
NSString* mainPathMjs = [mainPath stringByAppendingPathExtension:@"mjs"];
if ([fileManager fileExistsAtPath:mainPathMjs]) {
if (shouldLogBundleResolution()) {
NSLog(@"NativeScript BundleLoader resolved main: %@", mainPathMjs);
}
return std::string([mainPathMjs UTF8String]);
}

NSString* mainPathJs = [mainPath stringByAppendingPathExtension:@"js"];
if ([fileManager fileExistsAtPath:mainPathJs]) {
if (shouldLogBundleResolution()) {
NSLog(@"NativeScript BundleLoader resolved main: %@", mainPathJs);
}
return std::string([mainPathJs UTF8String]);
}
}
}
} else if (shouldLogBundleResolution()) {
NSLog(@"NativeScript BundleLoader failed to parse %@: %@", packageJsonPath, error);
}
}

// Fallback to app/index.js
NSString* indexPath = [appPath stringByAppendingPathComponent:@"index.js"];
if ([fileManager fileExistsAtPath:indexPath]) {
if (shouldLogBundleResolution()) {
NSLog(@"NativeScript BundleLoader resolved fallback main: %@", indexPath);
}
return std::string([indexPath UTF8String]);
}
} else if (shouldLogBundleResolution()) {
NSLog(@"NativeScript BundleLoader skipped resources path: %@ appPath=%@ exists=%d isDir=%d",
resourcesPath,
appPath,
[fileManager fileExistsAtPath:appPath],
isDir);
}

return "";
}

std::string resolveMainPath() {
NSMutableArray<NSString*>* candidates = [NSMutableArray array];
addCandidatePath(candidates, [[NSBundle mainBundle] resourcePath]);
addCandidatePath(candidates, resourcesPathForExecutable([[NSBundle mainBundle] executablePath]));

NSArray<NSString*>* arguments = [[NSProcessInfo processInfo] arguments];
if ([arguments count] > 0) {
addCandidatePath(candidates, resourcesPathForExecutable([arguments objectAtIndex:0]));
}

uint32_t executablePathLength = 0;
_NSGetExecutablePath(nullptr, &executablePathLength);
if (executablePathLength > 0) {
char* executablePathBuffer = static_cast<char*>(malloc(executablePathLength));
if (executablePathBuffer != nullptr) {
if (_NSGetExecutablePath(executablePathBuffer, &executablePathLength) == 0) {
addCandidatePath(candidates, resourcesPathForExecutable([NSString stringWithUTF8String:executablePathBuffer]));
}
free(executablePathBuffer);
}
}

NSString* currentDirectory = [[NSFileManager defaultManager] currentDirectoryPath];
addCandidatePath(candidates, currentDirectory);
addCandidatePath(candidates, [currentDirectory stringByAppendingPathComponent:@"Resources"]);
addCandidatePath(candidates, [[currentDirectory stringByDeletingLastPathComponent] stringByAppendingPathComponent:@"Resources"]);

for (NSString* resourcesPath in candidates) {
if (shouldLogBundleResolution()) {
NSLog(@"NativeScript BundleLoader candidate resources: %@", resourcesPath);
}
std::string mainPath = resolveMainPathInResources(resourcesPath);
if (!mainPath.empty()) {
return mainPath;
}
}

return "";
Expand Down
21 changes: 19 additions & 2 deletions NativeScript/ffi/hermes/NativeApiJsi.mm
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,13 @@ throw JSError(runtime,

const bool propertyGetterCall =
entry.hasMember && entry.member.property && count == 0;
if (propertyGetterCall) {
Value appearanceExpando = cachedAppearanceProxyPropertyValue(
runtime, bridge, receiver, entry.member.name);
if (!appearanceExpando.isUndefined()) {
return appearanceExpando;
}
}
const std::string* selectorNamePtr = &entry.selectorName;
const NativeApiMember* selectedMember =
entry.hasMember ? &entry.member : nullptr;
Expand Down Expand Up @@ -260,12 +267,15 @@ throw JSError(runtime,
// disown handling, or implicit NSError-out argument).
if (prepared->gsdEngineCallable && gsdDispatchClass == Nil &&
count == prepared->gsdEngineArgumentCount &&
!(!receiverIsClass && prepared->isInitMethod)) {
!(!receiverIsClass && prepared->isInitMethod) &&
!isPreparedStaticAppearanceSelector(*prepared)) {
auto invoker =
reinterpret_cast<ObjCGsdInvoker>(prepared->engineInvoker);
GsdObjCContext ctx{runtime, bridge, receiver, prepared->selector,
args, prepared->signature.returnType};
if (invoker(ctx)) {
cachePreparedAppearanceProxySetterValue(runtime, bridge, receiver,
*prepared, args, count);
return std::move(ctx.result);
}
}
Expand Down Expand Up @@ -293,8 +303,15 @@ throw JSError(runtime,
throw JSError(runtime,
"Objective-C selector requires a native receiver.");
}
return receiverHostObject->callPreparedObjectSelector(
Value result = receiverHostObject->callPreparedObjectSelector(
runtime, *prepared, args, count, gsdDispatchClass);
if (!receiverIsClass && prepared->isInitMethod) {
if (auto preserved = preservedNativeApiInitializerSelfReturn(
runtime, bridge, receiver, result, thisValue)) {
return *preserved;
}
}
return result;
});
}

Expand Down
3 changes: 2 additions & 1 deletion NativeScript/ffi/hermes/NativeApiJsiReactNative.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ inline NativeApiJsiConfig MakeReactNativeNativeApiJsiConfig(
config.metadataPath = metadataPath;
config.metadataPtr = metadataPtr;
config.globalName = globalName;
config.installGlobalSymbols = true;
config.installGlobalSymbols = false;
config.indexRuntimePointers = false;
config.invokeCallbacksOnNativeCallerThread = true;
config.scheduler = std::make_shared<ReactNativeCallInvokerScheduler>(
std::move(jsInvoker), std::move(uiInvoker));
Expand Down
38 changes: 36 additions & 2 deletions NativeScript/ffi/jsc/NativeApiJSCSelectorGroups.mm
Original file line number Diff line number Diff line change
Expand Up @@ -106,11 +106,17 @@ throw JSError(
// return value — bypassing all generic marshalling.
if (prepared.gsdEngineCallable && dispatchSuperClass == Nil &&
providedCount == prepared.gsdEngineArgumentCount &&
!initializerClassWrapper && !isNSErrorOutMethod) {
!initializerClassWrapper && !isNSErrorOutMethod &&
!isPreparedStaticAppearanceSelector(prepared)) {
auto invoker = reinterpret_cast<ObjCGsdInvoker>(prepared.engineInvoker);
GsdObjCContext ctx{runtime, bridge, receiver, prepared.selector,
runtime.context(), arguments, signature.returnType};
if (invoker(ctx)) {
if (providedCount > 0) {
Value setterValue = Value::borrowed(runtime, arguments[0]);
cachePreparedAppearanceProxySetterValue(runtime, bridge, receiver,
prepared, &setterValue, 1);
}
return ctx.result;
}
}
Expand All @@ -125,6 +131,11 @@ throw JSError(
if (tryCallFastEngineObjCSelector(runtime, bridge, receiver, prepared,
fastArgs, providedCount, Nil,
&fastResult)) {
cachePreparedAppearanceProxySetterValue(runtime, bridge, receiver,
prepared, fastArgs,
providedCount);
fastResult = tagPreparedStaticAppearanceSelectorResult(
runtime, bridge, receiver, prepared, std::move(fastResult));
return fastResult.local(runtime);
}
}
Expand Down Expand Up @@ -195,6 +206,11 @@ NativeApiReturnStorage returnStorage(
throw JSError(
runtime, errorMessage != nullptr ? errorMessage : "Unknown NSError");
}
if (providedCount > 0) {
Value setterValue = Value::borrowed(runtime, arguments[0]);
cachePreparedAppearanceProxySetterValue(runtime, bridge, receiver,
prepared, &setterValue, 1);
}
if (initializerClassWrapper) {
id resultObject = nil;
if (isObjectiveCObjectType(returnType)) {
Expand All @@ -209,6 +225,8 @@ throw JSError(
Value(runtime, *initializerClassWrapper));
}
}
tagPreparedStaticAppearanceNativeReturn(
runtime, bridge, receiver, prepared, returnType, returnStorage.data());
return setJSCEngineReturnValue(runtime, bridge, returnType,
returnStorage.data(), prepared.selectorName);
}
Expand Down Expand Up @@ -265,6 +283,13 @@ throw JSError(runtime,

const bool propertyGetterCall =
entry.hasMember && entry.member.property && argumentCount == 0;
if (propertyGetterCall) {
Value appearanceExpando = cachedAppearanceProxyPropertyValue(
runtime, data->bridge, receiver, entry.member.name);
if (!appearanceExpando.isUndefined()) {
return appearanceExpando.local(runtime);
}
}
const std::string* selectorNamePtr = &entry.selectorName;
const NativeApiMember* selectedMember =
entry.hasMember ? &entry.member : nullptr;
Expand Down Expand Up @@ -373,9 +398,18 @@ throw JSError(runtime,
data->cachedDispatchClass = dispatchClass;
}
}
return setJSCEnginePreparedObjCResult(
JSValueRef result = setJSCEnginePreparedObjCResult(
runtime, data->bridge, receiver, *prepared, receiverHostObject,
initializerClassWrapper, argumentCount, arguments, dispatchClass);
if (!data->receiverIsClass && prepared->isInitMethod &&
thisObject != nullptr) {
if (auto preserved = preservedNativeApiInitializerSelfReturn(
runtime, data->bridge, receiver, Value::borrowed(runtime, result),
Value::borrowed(runtime, thisObject))) {
return preserved->local(runtime);
}
}
return result;
} catch (const std::exception& error) {
engine::jscengine::setException(context, exception, error);
return JSValueMakeUndefined(context);
Expand Down
38 changes: 36 additions & 2 deletions NativeScript/ffi/quickjs/NativeApiQuickJSSelectorGroups.mm
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,17 @@ throw JSError(
// return value — bypassing all generic marshalling.
if (prepared.gsdEngineCallable && dispatchSuperClass == Nil &&
providedCount == prepared.gsdEngineArgumentCount &&
!initializerClassWrapper && !isNSErrorOutMethod) {
!initializerClassWrapper && !isNSErrorOutMethod &&
!isPreparedStaticAppearanceSelector(prepared)) {
auto invoker = reinterpret_cast<ObjCGsdInvoker>(prepared.engineInvoker);
GsdObjCContext ctx{runtime, bridge, receiver, prepared.selector,
runtime.context(), arguments, signature.returnType};
if (invoker(ctx)) {
if (providedCount > 0) {
Value setterValue = Value::borrowed(runtime, arguments[0]);
cachePreparedAppearanceProxySetterValue(runtime, bridge, receiver,
prepared, &setterValue, 1);
}
return ctx.result;
}
}
Expand All @@ -124,6 +130,11 @@ throw JSError(
if (tryCallFastEngineObjCSelector(runtime, bridge, receiver, prepared,
fastArgs, providedCount, Nil,
&fastResult)) {
cachePreparedAppearanceProxySetterValue(runtime, bridge, receiver,
prepared, fastArgs,
providedCount);
fastResult = tagPreparedStaticAppearanceSelectorResult(
runtime, bridge, receiver, prepared, std::move(fastResult));
return fastResult.local(runtime);
}
}
Expand Down Expand Up @@ -195,6 +206,11 @@ NativeApiReturnStorage returnStorage(
throw JSError(
runtime, errorMessage != nullptr ? errorMessage : "Unknown NSError");
}
if (providedCount > 0) {
Value setterValue = Value::borrowed(runtime, arguments[0]);
cachePreparedAppearanceProxySetterValue(runtime, bridge, receiver,
prepared, &setterValue, 1);
}
if (initializerClassWrapper) {
id resultObject = nil;
if (isObjectiveCObjectType(returnType)) {
Expand All @@ -209,6 +225,8 @@ throw JSError(
Value(runtime, *initializerClassWrapper));
}
}
tagPreparedStaticAppearanceNativeReturn(
runtime, bridge, receiver, prepared, returnType, returnStorage.data());
return setQuickJSEngineReturnValue(runtime, bridge, returnType,
returnStorage.data(),
prepared.selectorName);
Expand Down Expand Up @@ -287,6 +305,13 @@ throw JSError(runtime,

const bool propertyGetterCall =
entry.hasMember && entry.member.property && count == 0;
if (propertyGetterCall) {
Value appearanceExpando = cachedAppearanceProxyPropertyValue(
runtime, data->bridge, receiver, entry.member.name);
if (!appearanceExpando.isUndefined()) {
return appearanceExpando.local(runtime);
}
}
const std::string* selectorNamePtr = &entry.selectorName;
const NativeApiMember* selectedMember =
entry.hasMember ? &entry.member : nullptr;
Expand Down Expand Up @@ -386,9 +411,18 @@ throw JSError(runtime,
data->cachedDispatchClass = dispatchClass;
}
}
return setQuickJSEnginePreparedObjCResult(
JSValue result = setQuickJSEnginePreparedObjCResult(
runtime, data->bridge, receiver, *prepared, receiverHostObject,
initializerClassWrapper, count, argv, dispatchClass);
if (!data->receiverIsClass && prepared->isInitMethod) {
if (auto preserved = preservedNativeApiInitializerSelfReturn(
runtime, data->bridge, receiver, Value::borrowed(runtime, result),
Value::borrowed(runtime, thisValue))) {
JS_FreeValue(context, result);
return preserved->local(runtime);
}
}
return result;
} catch (const std::exception& error) {
return engine::quickjsengine::throwError(context, error);
}
Expand Down
Loading
Loading