diff --git a/NativeScript/NativeScript-Prefix.pch b/NativeScript/NativeScript-Prefix.pch index 1ce57293..e9875cc1 100644 --- a/NativeScript/NativeScript-Prefix.pch +++ b/NativeScript/NativeScript-Prefix.pch @@ -1,7 +1,7 @@ #ifndef NativeScript_Prefix_pch #define NativeScript_Prefix_pch -#define NATIVESCRIPT_VERSION "8.9.3" +#define NATIVESCRIPT_VERSION "9.0.0-alpha.10" #ifdef DEBUG #define SIZEOF_OFF_T 8 diff --git a/NativeScript/runtime/DevFlags.h b/NativeScript/runtime/DevFlags.h new file mode 100644 index 00000000..ac2b7ede --- /dev/null +++ b/NativeScript/runtime/DevFlags.h @@ -0,0 +1,13 @@ +#pragma once + +// Centralized development/runtime flags helpers usable across runtime sources. +// These read from app package.json via Runtime::GetAppConfigValue and other +// runtime configuration to determine behavior of dev features. + +namespace tns { + +// Returns true when verbose script/module loading logs should be emitted. +// Controlled by package.json setting: "logScriptLoading": true|false +bool IsScriptLoadingLogEnabled(); + +} // namespace tns diff --git a/NativeScript/runtime/DevFlags.mm b/NativeScript/runtime/DevFlags.mm new file mode 100644 index 00000000..55cf8634 --- /dev/null +++ b/NativeScript/runtime/DevFlags.mm @@ -0,0 +1,14 @@ +#import + +#include "DevFlags.h" +#include "Runtime.h" +#include "RuntimeConfig.h" + +namespace tns { + +bool IsScriptLoadingLogEnabled() { + id value = Runtime::GetAppConfigValue("logScriptLoading"); + return value ? [value boolValue] : false; +} + +} // namespace tns diff --git a/NativeScript/runtime/HMRSupport.h b/NativeScript/runtime/HMRSupport.h new file mode 100644 index 00000000..cf8af2a1 --- /dev/null +++ b/NativeScript/runtime/HMRSupport.h @@ -0,0 +1,61 @@ +#pragma once + +#include +#include + +// Forward declare v8 types to keep this header lightweight and avoid +// requiring V8 headers at include sites. +namespace v8 { +class Isolate; +template class Local; +class Object; +class Function; +class Context; +} + +namespace tns { + +// HMRSupport: Isolated helpers for minimal HMR (import.meta.hot) support. +// +// This module contains: +// - Per-module hot data store +// - Registration for accept/disable callbacks +// - Initializer to attach import.meta.hot to a module's import.meta +// +// Note: Triggering/dispatch is handled by the HMR system elsewhere. + +// Retrieve or create the per-module hot data object. +v8::Local GetOrCreateHotData(v8::Isolate* isolate, const std::string& key); + +// Register accept and dispose callbacks for a module key. +void RegisterHotAccept(v8::Isolate* isolate, const std::string& key, v8::Local cb); +void RegisterHotDispose(v8::Isolate* isolate, const std::string& key, v8::Local cb); + +// Optional: expose read helpers (may be useful for debugging/integration) +std::vector> GetHotAcceptCallbacks(v8::Isolate* isolate, const std::string& key); +std::vector> GetHotDisposeCallbacks(v8::Isolate* isolate, const std::string& key); + +// Attach a minimal import.meta.hot object to the provided import.meta object. +// The modulePath should be the canonical path used to key callback/data maps. +void InitializeImportMetaHot(v8::Isolate* isolate, + v8::Local context, + v8::Local importMeta, + const std::string& modulePath); + +// ───────────────────────────────────────────────────────────── +// Dev HTTP loader helpers (used during HMR only) +// These are isolated here so ModuleInternalCallbacks stays lean. +// +// Normalize HTTP(S) URLs for module registry keys. +// - Preserves versioning params for SFC endpoints (/@ns/sfc, /@ns/asm) +// - Drops cache-busting segments for /@ns/rt and /@ns/core +// - Drops query params for general app modules (/@ns/m) +std::string CanonicalizeHttpUrlKey(const std::string& url); + +// Minimal text fetch for dev HTTP ESM loader. Returns true on 2xx with non-empty body. +// - out: response body +// - contentType: Content-Type header if present +// - status: HTTP status code +bool HttpFetchText(const std::string& url, std::string& out, std::string& contentType, int& status); + +} // namespace tns diff --git a/NativeScript/runtime/HMRSupport.mm b/NativeScript/runtime/HMRSupport.mm new file mode 100644 index 00000000..55fa21e4 --- /dev/null +++ b/NativeScript/runtime/HMRSupport.mm @@ -0,0 +1,330 @@ +#include "HMRSupport.h" +#import +#include +#include "DevFlags.h" + +#include +#include +#include +#include "Helpers.h" + +// Use centralized dev flags helper for logging + +namespace tns { + +static inline bool StartsWith(const std::string& s, const char* prefix) { + size_t n = strlen(prefix); + return s.size() >= n && s.compare(0, n, prefix) == 0; +} + +// Per-module hot data and callbacks. Keyed by canonical module path. +static std::unordered_map> g_hotData; +static std::unordered_map>> g_hotAccept; +static std::unordered_map>> g_hotDispose; + +v8::Local GetOrCreateHotData(v8::Isolate* isolate, const std::string& key) { + auto it = g_hotData.find(key); + if (it != g_hotData.end()) { + if (!it->second.IsEmpty()) { + return it->second.Get(isolate); + } + } + v8::Local obj = v8::Object::New(isolate); + g_hotData[key].Reset(isolate, obj); + return obj; +} + +void RegisterHotAccept(v8::Isolate* isolate, const std::string& key, v8::Local cb) { + if (cb.IsEmpty()) return; + g_hotAccept[key].emplace_back(v8::Global(isolate, cb)); +} + +void RegisterHotDispose(v8::Isolate* isolate, const std::string& key, v8::Local cb) { + if (cb.IsEmpty()) return; + g_hotDispose[key].emplace_back(v8::Global(isolate, cb)); +} + +std::vector> GetHotAcceptCallbacks(v8::Isolate* isolate, const std::string& key) { + std::vector> out; + auto it = g_hotAccept.find(key); + if (it != g_hotAccept.end()) { + for (auto& gfn : it->second) { + if (!gfn.IsEmpty()) out.push_back(gfn.Get(isolate)); + } + } + return out; +} + +std::vector> GetHotDisposeCallbacks(v8::Isolate* isolate, const std::string& key) { + std::vector> out; + auto it = g_hotDispose.find(key); + if (it != g_hotDispose.end()) { + for (auto& gfn : it->second) { + if (!gfn.IsEmpty()) out.push_back(gfn.Get(isolate)); + } + } + return out; +} + +void InitializeImportMetaHot(v8::Isolate* isolate, + v8::Local context, + v8::Local importMeta, + const std::string& modulePath) { + using v8::Function; + using v8::FunctionCallbackInfo; + using v8::Local; + using v8::Object; + using v8::String; + using v8::Value; + + // Ensure context scope for property creation + v8::HandleScope scope(isolate); + + // Helper to capture key in function data + auto makeKeyData = [&](const std::string& key) -> Local { + return tns::ToV8String(isolate, key.c_str()); + }; + + // accept([deps], cb?) — we register cb if provided; deps ignored for now + auto acceptCb = [](const FunctionCallbackInfo& info) { + v8::Isolate* iso = info.GetIsolate(); + Local data = info.Data(); + std::string key; + if (!data.IsEmpty()) { + v8::String::Utf8Value s(iso, data); + key = *s ? *s : ""; + } + v8::Local cb; + if (info.Length() >= 1 && info[0]->IsFunction()) { + cb = info[0].As(); + } else if (info.Length() >= 2 && info[1]->IsFunction()) { + cb = info[1].As(); + } + if (!cb.IsEmpty()) { + RegisterHotAccept(iso, key, cb); + } + // Return undefined + info.GetReturnValue().Set(v8::Undefined(iso)); + }; + + // dispose(cb) — register disposer + auto disposeCb = [](const FunctionCallbackInfo& info) { + v8::Isolate* iso = info.GetIsolate(); + Local data = info.Data(); + std::string key; + if (!data.IsEmpty()) { v8::String::Utf8Value s(iso, data); key = *s ? *s : ""; } + if (info.Length() >= 1 && info[0]->IsFunction()) { + RegisterHotDispose(iso, key, info[0].As()); + } + info.GetReturnValue().Set(v8::Undefined(iso)); + }; + + // decline() — mark declined (no-op for now) + auto declineCb = [](const FunctionCallbackInfo& info) { + info.GetReturnValue().Set(v8::Undefined(info.GetIsolate())); + }; + + // invalidate() — no-op for now + auto invalidateCb = [](const FunctionCallbackInfo& info) { + info.GetReturnValue().Set(v8::Undefined(info.GetIsolate())); + }; + + Local hot = Object::New(isolate); + // Stable flags + hot->CreateDataProperty(context, tns::ToV8String(isolate, "data"), + GetOrCreateHotData(isolate, modulePath)).Check(); + hot->CreateDataProperty(context, tns::ToV8String(isolate, "prune"), + v8::Boolean::New(isolate, false)).Check(); + // Methods + hot->CreateDataProperty( + context, tns::ToV8String(isolate, "accept"), + v8::Function::New(context, acceptCb, makeKeyData(modulePath)).ToLocalChecked()).Check(); + hot->CreateDataProperty( + context, tns::ToV8String(isolate, "dispose"), + v8::Function::New(context, disposeCb, makeKeyData(modulePath)).ToLocalChecked()).Check(); + hot->CreateDataProperty( + context, tns::ToV8String(isolate, "decline"), + v8::Function::New(context, declineCb, makeKeyData(modulePath)).ToLocalChecked()).Check(); + hot->CreateDataProperty( + context, tns::ToV8String(isolate, "invalidate"), + v8::Function::New(context, invalidateCb, makeKeyData(modulePath)).ToLocalChecked()).Check(); + + // Attach to import.meta + importMeta->CreateDataProperty( + context, tns::ToV8String(isolate, "hot"), + hot).Check(); +} + +// ───────────────────────────────────────────────────────────── +// Dev HTTP loader helpers + +std::string CanonicalizeHttpUrlKey(const std::string& url) { + if (!(StartsWith(url, "http://") || StartsWith(url, "https://"))) { + return url; + } + // Drop fragment entirely + size_t hashPos = url.find('#'); + std::string noHash = (hashPos == std::string::npos) ? url : url.substr(0, hashPos); + + // Locate path start and query start + size_t schemePos = noHash.find("://"); + if (schemePos == std::string::npos) { + // Unexpected shape; fall back to removing whole query + size_t q = noHash.find('?'); + return (q == std::string::npos) ? noHash : noHash.substr(0, q); + } + size_t pathStart = noHash.find('/', schemePos + 3); + if (pathStart == std::string::npos) { + // No path; nothing to normalize + return noHash; + } + size_t qPos = noHash.find('?', pathStart); + std::string originAndPath = (qPos == std::string::npos) ? noHash : noHash.substr(0, qPos); + std::string query = (qPos == std::string::npos) ? std::string() : noHash.substr(qPos + 1); + + // Decide behavior based on path prefix + std::string pathOnly = noHash.substr(pathStart, (qPos == std::string::npos) ? std::string::npos : (qPos - pathStart)); + bool isAppMod = pathOnly.find("/ns/m") != std::string::npos; + bool isSfcMod = pathOnly.find("/ns/sfc") != std::string::npos; + bool isSfcAsm = pathOnly.find("/ns/asm") != std::string::npos; + bool isRt = pathOnly.find("/ns/rt") != std::string::npos; + bool isCore = pathOnly.find("/ns/core") != std::string::npos; + + if (query.empty()) return originAndPath; + + if (isAppMod) { + return originAndPath; + } + + if (isRt || isCore) { + std::string canonical = originAndPath; + const char* needle = isRt ? "/ns/rt" : "/ns/core"; + size_t pos = canonical.find(needle); + if (pos != std::string::npos) { + canonical = canonical.substr(0, pos) + std::string(needle); + } + return canonical; + } + + if (isSfcMod || isSfcAsm) { + std::vector keptParams; + size_t startPos = 0; + while (startPos <= query.size()) { + size_t amp = query.find('&', startPos); + std::string pair = (amp == std::string::npos) ? query.substr(startPos) : query.substr(startPos, amp - startPos); + if (!pair.empty()) { + size_t eq = pair.find('='); + std::string name = (eq == std::string::npos) ? pair : pair.substr(0, eq); + if (!(name == "import")) { + keptParams.push_back(pair); + } + } + if (amp == std::string::npos) break; + startPos = amp + 1; + } + if (keptParams.empty()) return originAndPath; + std::string rebuilt = originAndPath + "?"; + for (size_t i = 0; i < keptParams.size(); i++) { + rebuilt += keptParams[i]; + if (i + 1 < keptParams.size()) rebuilt += "&"; + } + return rebuilt; + } + + // Other URLs: keep all params except Vite's import marker; sort for stability + std::vector kept; + size_t start = 0; + while (start <= query.size()) { + size_t amp = query.find('&', start); + std::string pair = (amp == std::string::npos) ? query.substr(start) : query.substr(start, amp - start); + if (!pair.empty()) { + size_t eq = pair.find('='); + std::string name = (eq == std::string::npos) ? pair : pair.substr(0, eq); + if (!(name == "import")) kept.push_back(pair); + } + if (amp == std::string::npos) break; + start = amp + 1; + } + if (kept.empty()) return originAndPath; + std::sort(kept.begin(), kept.end()); + std::string rebuilt = originAndPath + "?"; + for (size_t i = 0; i < kept.size(); i++) { + if (i > 0) rebuilt += "&"; + rebuilt += kept[i]; + } + return rebuilt; +} + +bool HttpFetchText(const std::string& url, std::string& out, std::string& contentType, int& status) { + @autoreleasepool { + NSURL* u = [NSURL URLWithString:[NSString stringWithUTF8String:url.c_str()]]; + if (!u) { status = 0; return false; } + + __block NSError* err = nil; + __block NSInteger httpStatusLocal = 0; + __block std::string contentTypeLocal; + __block std::string bodyLocal; + + auto fetchOnce = ^BOOL(NSURL* reqUrl) { + bodyLocal.clear(); + err = nil; + httpStatusLocal = 0; + contentTypeLocal.clear(); + NSURLSessionConfiguration* cfg = [NSURLSessionConfiguration defaultSessionConfiguration]; + cfg.HTTPAdditionalHeaders = @{ @"Accept": @"application/javascript, text/javascript, */*;q=0.1", + @"Accept-Encoding": @"identity" }; + cfg.timeoutIntervalForRequest = 5.0; + cfg.timeoutIntervalForResource = 5.0; + NSURLSession* session = [NSURLSession sessionWithConfiguration:cfg]; + dispatch_semaphore_t sema = dispatch_semaphore_create(0); + NSURLSessionDataTask* task = [session dataTaskWithURL:reqUrl + completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) { + @autoreleasepool { + err = error; + if ([response isKindOfClass:[NSHTTPURLResponse class]]) { + httpStatusLocal = ((NSHTTPURLResponse*)response).statusCode; + NSString* ct = ((NSHTTPURLResponse*)response).allHeaderFields[@"Content-Type"]; + if (ct) { contentTypeLocal = std::string([ct UTF8String] ?: ""); } + } + if (data) { + const void* bytes = [data bytes]; + NSUInteger len = [data length]; + if (bytes && len > 0) { + bodyLocal.assign(static_cast(bytes), static_cast(len)); + } + } + } + dispatch_semaphore_signal(sema); + }]; + [task resume]; + dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(6 * NSEC_PER_SEC)); + dispatch_semaphore_wait(sema, timeout); + [session finishTasksAndInvalidate]; + return err == nil && !bodyLocal.empty(); + }; + + BOOL ok = fetchOnce(u); + if (!ok) { + if (tns::IsScriptLoadingLogEnabled()) { Log(@"[http-loader] retrying %s after initial fetch error", url.c_str()); } + usleep(120 * 1000); + ok = fetchOnce(u); + } + + status = (int)httpStatusLocal; + contentType = contentTypeLocal; + if (!ok || status < 200 || status >= 300) { + return false; + } + + out.swap(bodyLocal); + if (out.empty()) return false; + if (tns::IsScriptLoadingLogEnabled()) { + unsigned long long blen = (unsigned long long)out.size(); + const char* ctstr = contentType.empty() ? "" : contentType.c_str(); + Log(@"[http-loader] fetched status=%ld content-type=%s bytes=%llu", (long)status, ctstr, blen); + } + return true; + } +} + +} // namespace tns diff --git a/NativeScript/runtime/ModuleInternal.mm b/NativeScript/runtime/ModuleInternal.mm index 810a673a..ad7533de 100644 --- a/NativeScript/runtime/ModuleInternal.mm +++ b/NativeScript/runtime/ModuleInternal.mm @@ -1,5 +1,5 @@ #include "ModuleInternal.h" -#include +#import #include #include #include @@ -9,6 +9,7 @@ #include "Helpers.h" #include "ModuleInternalCallbacks.h" // for ResolveModuleCallback #include "NativeScriptException.h" +#include "DevFlags.h" #include "Runtime.h" // for GetAppConfigValue #include "RuntimeConfig.h" @@ -35,6 +36,26 @@ bool IsESModule(const std::string& path) { !(path.size() >= 8 && path.compare(path.size() - 8, 8, ".mjs.map") == 0); } +// Normalize file system paths to a canonical representation so lookups in +// g_moduleRegistry remain consistent regardless of how the path was provided. +static std::string NormalizePath(const std::string& path) { + if (path.empty()) { + return path; + } + + NSString* nsPath = [NSString stringWithUTF8String:path.c_str()]; + if (nsPath == nil) { + return path; + } + + NSString* standardized = [nsPath stringByStandardizingPath]; + if (standardized == nil) { + return path; + } + + return std::string([standardized UTF8String]); +} + // Helper function to resolve main entry from package.json with proper extension handling std::string ResolveMainEntryFromPackageJson(const std::string& baseDir) { // Get the main value from package.json @@ -144,12 +165,10 @@ bool IsESModule(const std::string& path) { } } - // Check if this is an ES module (.mjs) and handle it directly + // ES module fast path if (IsESModule(path)) { - // For ES modules, use LoadESModule directly instead of require() TryCatch tc(isolate); Local moduleNamespace; - try { moduleNamespace = ModuleInternal::LoadESModule(isolate, path); } catch (const NativeScriptException& ex) { @@ -158,23 +177,20 @@ bool IsESModule(const std::string& path) { Log(@"Error loading ES module: %s", path.c_str()); Log(@"Exception: %s", ex.getMessage().c_str()); Log(@"***** End stack trace - continuing execution *****"); - Log(@"Debug mode - ES module loading failed, but telling iOS it succeeded to prevent " - @"app termination"); - return true; // LIE TO iOS - return success to prevent app termination + Log(@"Debug mode - ES module loading failed, but telling iOS it succeeded to prevent app termination"); + return true; // avoid termination in debug } else { return false; } } - if (moduleNamespace.IsEmpty()) { if (RuntimeConfig.IsDebug) { Log(@"Debug mode - ES module returned empty namespace, but telling iOS it succeeded"); - return true; // LIE TO iOS - return success to prevent app termination + return true; } else { return false; } } - return true; // ES module loaded successfully } @@ -280,6 +296,17 @@ bool IsESModule(const std::string& path) { NSString* fullPath = nil; try { + // Guard: URL-based modules must be loaded via dynamic import() in dev HTTP ESM mode. + if (info.Length() > 0 && info[0]->IsString()) { + v8::String::Utf8Value s(isolate, info[0]); + if (*s) { + moduleName.assign(*s, s.length()); + if (moduleName.rfind("http://", 0) == 0 || moduleName.rfind("https://", 0) == 0) { + std::string msg = std::string("NativeScript: require() of URL module is not supported: ") + moduleName + ". Use dynamic import() instead."; + throw NativeScriptException(msg.c_str()); + } + } + } ModuleInternal* moduleInternal = static_cast(info.Data().As()->Value()); @@ -307,6 +334,7 @@ bool IsESModule(const std::string& path) { fullPath = [[NSString stringWithUTF8String:RuntimeConfig.ApplicationPath.c_str()] stringByAppendingPathComponent:[NSString stringWithUTF8String:moduleName.c_str()]]; } else { + // Default: resolve in tns_modules (shared folder override removed) NSString* tnsModulesPath = [[NSString stringWithUTF8String:RuntimeConfig.ApplicationPath.c_str()] stringByAppendingPathComponent:@"tns_modules"]; @@ -329,6 +357,7 @@ bool IsESModule(const std::string& path) { fullPath = [NSString stringWithUTF8String:moduleName.c_str()]; } + NSString* fileNameOnly = [fullPath lastPathComponent]; NSString* pathOnly = [fullPath stringByDeletingLastPathComponent]; @@ -668,21 +697,23 @@ throw NativeScriptException(isolate, } Local ModuleInternal::LoadScript(Isolate* isolate, const std::string& path) { - if (IsESModule(path)) { + std::string canonicalPath = NormalizePath(path); + + if (IsESModule(canonicalPath)) { // Treat all .mjs files as standard ES modules. - return ModuleInternal::LoadESModule(isolate, path); + return ModuleInternal::LoadESModule(isolate, canonicalPath); } - Local