Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
02c963d
feat: HMR dev-sessions, ESM resolver hardening, dev-mode runtime globals
NathanWalker May 22, 2026
9f05055
fix: visionos build deprecation
NathanWalker Jun 14, 2026
f0cb3a1
feat: HMR robustness and additional tests
NathanWalker Jun 16, 2026
c3c9dc6
test: unit test improvements
NathanWalker Jun 26, 2026
f83ea0e
fix(worker): surface worker entry-script load errors to worker.onerror
NathanWalker Jun 27, 2026
3ef34f0
refactor(runtime): key module registries by v8::Isolate, not thread_l…
NathanWalker Jun 28, 2026
c7c7ce2
refactor(runtime): reduce surface to a mechanism-only dev-loader cont…
NathanWalker Jul 3, 2026
803ba5d
feat(runtime): __NS_DEV__.seedModuleBodies for batch prewarm seeding …
NathanWalker Jul 4, 2026
c0d3a75
feat(runtime): harden the HTTP session loader
NathanWalker Jul 9, 2026
d9d4b51
fix(worker): buffer inbound messages until the entry script installs …
NathanWalker Jul 11, 2026
a19a381
fix(module): build the optional-module placeholder without string int…
NathanWalker Jul 11, 2026
ac45d41
fix(module): preserve embedded NUL bytes in dev-session module source
NathanWalker Jul 11, 2026
a7dbba7
fix(module): unify IsLikelyOptionalModule and exclude explicit file e…
NathanWalker Jul 21, 2026
885ac28
fix(module): restore the optional-module placeholder call sites
NathanWalker Jul 29, 2026
2ad853d
docs(hmr): correct the V8 version pinned in the loader's constraint c…
NathanWalker Jul 29, 2026
e58c152
test(runner): await promise-returning Jasmine specs
NathanWalker Jul 29, 2026
d4a6a23
fix(module): adopt thenables where JS hands the loader a promise
NathanWalker Jul 29, 2026
c342282
test(esm): give the blob-URL module spec a Blob to work with
NathanWalker Jul 29, 2026
a1aabec
style(runtime): format this branch's runtime changes with clang-format
NathanWalker Jul 29, 2026
6c262f0
refactor(module): strip debug-era scaffolding from the dynamic-import…
NathanWalker Jul 29, 2026
cc6ad54
refactor(module): drop the native '@/' -> '/src/' alias expansion
NathanWalker Jul 29, 2026
05b59f6
refactor(module)!: reject missing bare import() specifiers instead of…
NathanWalker Jul 29, 2026
ece51eb
perf(runtime): move this branch's hash containers to robin_hood
NathanWalker Jul 29, 2026
4fea219
refactor(runtime)!: slim the __NS_DEV__ contract and drop ns-vendor://
NathanWalker Jul 30, 2026
cd6031c
chore: cleanup
NathanWalker Jul 30, 2026
798b6b6
cleanup
NathanWalker Jul 30, 2026
5ebd33e
feat(runtime): async module-graph loader replaces the prewarm/prefetc…
NathanWalker Jul 30, 2026
02b2a89
refactor: move embedded runtime JS to real .js files compiled in via …
edusperoni Jul 30, 2026
000240d
refactor: compile builtins as function bodies with a fixed binding pa…
edusperoni Jul 30, 2026
8add3aa
refactor: export builtins via module.exports instead of top-level return
edusperoni Jul 30, 2026
04b16a2
feat: budgeted console formatter (inspect builtin) replacing JSON-bas…
edusperoni Jul 30, 2026
8444396
feat(runtime): expose the dev-loader surface as the ns:runtime builti…
NathanWalker Jul 30, 2026
c14a5a6
test: align network-timeout and bare-specifier specs with the async l…
NathanWalker Jul 31, 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
40 changes: 40 additions & 0 deletions .github/workflows/npm_release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,46 @@ jobs:
# "Existing file at -resultBundlePath".
on_retry_command: rm -rf $TEST_FOLDER/test_results_attempt1.xcresult; mv $TEST_FOLDER/test_results.xcresult $TEST_FOLDER/test_results_attempt1.xcresult 2>/dev/null; for f in $TEST_FOLDER/test_results*; do [ "$f" = "$TEST_FOLDER/test_results_attempt1.xcresult" ] || rm -rf "$f"; done; xcrun simctl shutdown all
new_command_on_retry: xcodebuild -project v8ios.xcodeproj -scheme TestRunner -resultBundlePath $TEST_FOLDER/test_results -destination platform\=iOS\ Simulator,OS\=latest,name\=iPhone\ 16\ Pro build test
# When the runtime suite fails it is almost always because the in-app
# Jasmine run died before POSTing results (crash or hang). The xcresult is
# black-box and captures nothing from inside the app, so collect the two
# things that actually explain it: the native crash report (.ips) and the
# simulator's unified log (the app's console.log / last spec before a stall).
# The watchdog in TestRunnerTests.swift prints which artifact to look at.
- name: Collect crash reports & simulator log (on failure)
if: ${{ failure() }}
run: |
DIAG="$TEST_FOLDER/diagnostics"
mkdir -p "$DIAG"
# Simulator app crashes land in the host's DiagnosticReports.
cp -R ~/Library/Logs/DiagnosticReports/. "$DIAG/DiagnosticReports/" 2>/dev/null || true
cp -R ~/Library/Logs/CoreSimulator/. "$DIAG/CoreSimulator/" 2>/dev/null || true
# Unified log = the app's console output (so the last spec before a hang
# is visible even when nothing was POSTed). `log collect` needs a booted
# device; don't rely on the `booted` alias (the prior collect failed
# because the sim wasn't booted at that moment). Resolve a concrete UDID
# — prefer one already booted from the test run, else the test device,
# booting it so the persisted log store can be collected.
UDID="$(xcrun simctl list devices booted | grep -oE '[0-9A-Fa-f-]{36}' | head -1)"
if [ -z "$UDID" ]; then
UDID="$(xcrun simctl list devices 'iPhone 16 Pro' | grep -oE '[0-9A-Fa-f-]{36}' | head -1)"
[ -n "$UDID" ] && xcrun simctl boot "$UDID" 2>/dev/null || true
[ -n "$UDID" ] && xcrun simctl bootstatus "$UDID" 2>/dev/null || true
fi
if [ -n "$UDID" ]; then
echo "Collecting unified log from simulator $UDID"
xcrun simctl spawn "$UDID" log collect --output "$DIAG/simulator.logarchive" 2>/dev/null || true
else
echo "No simulator UDID resolved; skipping logarchive collection."
fi
echo "Collected diagnostics:"; ls -laR "$DIAG" 2>/dev/null || true
- name: Upload test diagnostics (on failure)
if: ${{ failure() }}
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: test-diagnostics
path: ${{ env.TEST_FOLDER }}/diagnostics
if-no-files-found: ignore
- name: Validate Test Results
run: |
xcparse attachments $TEST_FOLDER/test_results.xcresult $TEST_FOLDER/test-out
Expand Down
18 changes: 18 additions & 0 deletions NativeScript/NativeScript.mm
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "inspector/JsV8InspectorClient.h"
#include "runtime/Console.h"
#include "runtime/Helpers.h"
#include "runtime/ModuleInternalCallbacks.h"
#include "runtime/Runtime.h"
#include "runtime/RuntimeConfig.h"
#include "runtime/Tasks.h"
Expand Down Expand Up @@ -43,6 +44,23 @@ - (void)runMainApplication {

CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, true);
tns::Tasks::Drain();

// Async-pipeline boot handoff. For UI apps Tasks::Drain() invokes
// UIApplicationMain and never returns — the app's main runloop services
// any in-flight async module loads. When Drain returns (the entry never
// called UIApplicationMain — e.g. a top-level-await entry still loading
// its graph), pump a manual runloop until the pending module work
// settles, Node-like. A load completion may itself register the
// UIApplicationMain task, so drain after each slice; if that drain calls
// UIApplicationMain, it takes over from here and never returns.
if (tns::HasPendingAsyncModuleGraphWork()) {
const CFAbsoluteTime deadline = CFAbsoluteTimeGetCurrent() + 120.0;
while (tns::HasPendingAsyncModuleGraphWork() && CFAbsoluteTimeGetCurrent() < deadline) {
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.01, true);
tns::Tasks::Drain();
}
tns::Tasks::Drain();
}
}

- (bool)liveSync {
Expand Down
15 changes: 15 additions & 0 deletions NativeScript/runtime/ConcurrentQueue.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,21 @@ std::vector<std::shared_ptr<worker::Message>> ConcurrentQueue::PopAll() {
return messages;
}

bool ConcurrentQueue::IsEmpty() {
std::unique_lock<std::mutex> mlock(this->mutex_);
return this->messagesQueue_.empty();
}

void ConcurrentQueue::Signal() {
// Mirrors Push()'s validity handling instead of SignalAndWakeUp()'s
// assert: a retry racing Terminate() must be a silent no-op.
if (this->runLoopTasksSource_ == nullptr ||
!CFRunLoopSourceIsValid(this->runLoopTasksSource_)) {
return;
}
this->SignalAndWakeUp();
}

void ConcurrentQueue::SignalAndWakeUp() {
if (this->runLoopTasksSource_ != nullptr) {
tns::Assert(CFRunLoopSourceIsValid(this->runLoopTasksSource_));
Expand Down
6 changes: 6 additions & 0 deletions NativeScript/runtime/ConcurrentQueue.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ struct ConcurrentQueue {
void Initialize(CFRunLoopRef runLoop, void (*performWork)(void*), void* info);
void Push(std::shared_ptr<worker::Message> message);
std::vector<std::shared_ptr<worker::Message>> PopAll();
bool IsEmpty();
// Re-arm the drain source without enqueueing a new message — used to
// retry delivery of already-queued messages (e.g. a worker whose entry
// script hasn't installed `onmessage` yet). Safe from any thread; a
// no-op once terminated.
void Signal();
void Terminate();
private:
std::queue<std::shared_ptr<worker::Message>> messagesQueue_;
Expand Down
6 changes: 6 additions & 0 deletions NativeScript/runtime/DataWrapper.h
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,9 @@ class WorkerWrapper : public BaseDataWrapper {
const std::string& stackTrace,
int lineNumber, bool async = true);
void PostMessage(std::shared_ptr<worker::Message> message);
// Re-arm the message drain without enqueueing — used by the deferred-drain
// retry when the worker's entry script hasn't installed `onmessage` yet.
void SignalMessageDrain();
void Close();
void Terminate();

Expand All @@ -566,6 +569,9 @@ class WorkerWrapper : public BaseDataWrapper {
std::atomic<bool> isTerminating_;
std::atomic<bool> isDisposed_;
std::atomic<bool> isWeak_;
// True while a deferred drain retry is in flight (see DrainPendingTasks) —
// prevents stacking one retry per drain attempt.
std::atomic<bool> drainRetryPending_;
std::function<void(v8::Isolate*, v8::Local<v8::Object> thiz,
std::shared_ptr<worker::Message>)>
onMessage_;
Expand Down
8 changes: 8 additions & 0 deletions NativeScript/runtime/DevFlags.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ namespace tns {
// Controlled by package.json setting: "logScriptLoading": true|false
bool IsScriptLoadingLogEnabled();

// HTTP module loader flags
//
// Returns true when one log line should be emitted per HTTP fetch URL.
// Default OFF because the volume is high (one line per fetch, hundreds per
// cold boot, hundreds per HMR refresh). Opt in via package.json /
// nativescript.config: "httpFetchUrlLog": true|false
bool IsHttpFetchUrlLogEnabled();

// Security config

// In debug mode (RuntimeConfig.IsDebug): always returns true.
Expand Down
63 changes: 52 additions & 11 deletions NativeScript/runtime/DevFlags.mm
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
#import <Foundation/Foundation.h>

#include <mutex>
#include <vector>
#include "DevFlags.h"
#include "Helpers.h"
#include "Runtime.h"
#include "RuntimeConfig.h"
#include <vector>
#include <mutex>

namespace tns {

Expand All @@ -13,16 +14,56 @@ bool IsScriptLoadingLogEnabled() {
return value ? [value boolValue] : false;
}

// HTTP module loader flags

// Default OFF because the volume is high (one line per fetch, hundreds per
// cold boot, hundreds per HMR refresh). Opt in via `nativescript.config.ts`:
//
// export default {
// httpFetchUrlLog: true, // turn on for diagnosis only
// …
// };
bool IsHttpFetchUrlLogEnabled() {
static std::once_flag s_initFlag;
static bool s_enabled = false;
std::call_once(s_initFlag, []() {
@autoreleasepool {
id value = Runtime::GetAppConfigValue("httpFetchUrlLog");
if (value && [value respondsToSelector:@selector(boolValue)]) {
s_enabled = [value boolValue];
}
}
if (IsScriptLoadingLogEnabled()) {
Log(@"[http-loader] fetch-url-log=%s", s_enabled ? "enabled" : "disabled");
}
});
return s_enabled;
}

// Security config

static std::once_flag s_securityConfigInitFlag;
static bool s_allowRemoteModules = false;
static std::vector<std::string> s_remoteModuleAllowlist;

// Helper to check if a URL starts with a given prefix
static bool UrlStartsWith(const std::string& url, const std::string& prefix) {
if (prefix.size() > url.size()) return false;
return url.compare(0, prefix.size(), prefix) == 0;
// Returns true when `url` is authorized by allowlist `entry`.
//
// This is intentionally stricter than a raw string-prefix test: after the
// matched entry text, the next character in `url` must be a URL-component
// boundary ('/', '?', or '#'), the URL must end exactly at the entry, or the
// entry must itself end in '/'. That refuses lookalike-host and lookalike-port
// bypasses — an entry of "https://cdn.example.com" must NOT authorize
// "https://cdn.example.com.attacker.com/x.js" or
// "https://cdn.example.com:9999/x.js". To allow a specific port, include it in
// the allowlist entry (deny-by-default for anything not explicitly listed).
static bool RemoteUrlMatchesAllowlistEntry(const std::string& url, const std::string& entry) {
if (entry.empty()) return false;
if (url.size() < entry.size()) return false;
if (url.compare(0, entry.size(), entry) != 0) return false;
if (url.size() == entry.size()) return true; // exact match
if (entry.back() == '/') return true; // entry ended at a boundary
const char next = url[entry.size()];
return next == '/' || next == '?' || next == '#';
}

void InitializeSecurityConfig() {
Expand Down Expand Up @@ -83,14 +124,14 @@ bool IsRemoteUrlAllowed(const std::string& url) {
if (s_remoteModuleAllowlist.empty()) {
return true;
}
// Check if URL matches any allowlist prefix
for (const std::string& prefix : s_remoteModuleAllowlist) {
if (UrlStartsWith(url, prefix)) {

// Check if URL matches any allowlist entry on a URL-component boundary.
for (const std::string& entry : s_remoteModuleAllowlist) {
if (RemoteUrlMatchesAllowlistEntry(url, entry)) {
return true;
}
}

return false;
}

Expand Down
Loading
Loading