Skip to content

Commit 7dff8ee

Browse files
committed
feat: Blob URL support
1 parent fb7833b commit 7dff8ee

File tree

2 files changed

+435
-0
lines changed

2 files changed

+435
-0
lines changed

NativeScript/runtime/ModuleInternalCallbacks.mm

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1912,6 +1912,178 @@ static bool IsDocumentsPath(const std::string& path) {
19121912
}
19131913
}
19141914

1915+
// ── Blob URL support (e.g., blob:nativescript/<uuid>) ──
1916+
// Also useful for HMR updates where we can load a blob URL
1917+
// We retrieve the blob content from the global BLOB_STORE via URL.InternalAccessor.getData()
1918+
// and compile/execute it as an ES module.
1919+
if (!normalizedSpec.empty() && StartsWith(normalizedSpec, "blob:nativescript/")) {
1920+
if (IsScriptLoadingLogEnabled()) {
1921+
Log(@"[dyn-import][blob] trying blob URL %s", normalizedSpec.c_str());
1922+
}
1923+
1924+
// Call URL.InternalAccessor.getData(url) to retrieve the blob data
1925+
v8::TryCatch tc(isolate);
1926+
v8::Local<v8::Object> globalObj = context->Global();
1927+
1928+
// Get URL constructor
1929+
v8::Local<v8::Value> urlCtorVal;
1930+
if (!globalObj->Get(context, tns::ToV8String(isolate, "URL")).ToLocal(&urlCtorVal) || !urlCtorVal->IsFunction()) {
1931+
if (IsScriptLoadingLogEnabled()) {
1932+
Log(@"[dyn-import][blob] URL constructor not found");
1933+
}
1934+
resolver->Reject(context, v8::Exception::Error(tns::ToV8String(isolate, "URL constructor not available"))).FromMaybe(false);
1935+
return scope.Escape(resolver->GetPromise());
1936+
}
1937+
v8::Local<v8::Object> urlCtor = urlCtorVal.As<v8::Object>();
1938+
1939+
// Get URL.InternalAccessor
1940+
v8::Local<v8::Value> internalAccessorVal;
1941+
if (!urlCtor->Get(context, tns::ToV8String(isolate, "InternalAccessor")).ToLocal(&internalAccessorVal) || !internalAccessorVal->IsObject()) {
1942+
if (IsScriptLoadingLogEnabled()) {
1943+
Log(@"[dyn-import][blob] URL.InternalAccessor not found");
1944+
}
1945+
resolver->Reject(context, v8::Exception::Error(tns::ToV8String(isolate, "URL.InternalAccessor not available"))).FromMaybe(false);
1946+
return scope.Escape(resolver->GetPromise());
1947+
}
1948+
v8::Local<v8::Object> internalAccessor = internalAccessorVal.As<v8::Object>();
1949+
1950+
// Get URL.InternalAccessor.getData function
1951+
v8::Local<v8::Value> getDataVal;
1952+
if (!internalAccessor->Get(context, tns::ToV8String(isolate, "getData")).ToLocal(&getDataVal) || !getDataVal->IsFunction()) {
1953+
if (IsScriptLoadingLogEnabled()) {
1954+
Log(@"[dyn-import][blob] URL.InternalAccessor.getData not found");
1955+
}
1956+
resolver->Reject(context, v8::Exception::Error(tns::ToV8String(isolate, "URL.InternalAccessor.getData not available"))).FromMaybe(false);
1957+
return scope.Escape(resolver->GetPromise());
1958+
}
1959+
v8::Local<v8::Function> getDataFn = getDataVal.As<v8::Function>();
1960+
1961+
// Call getData(url)
1962+
v8::Local<v8::Value> urlArg = tns::ToV8String(isolate, normalizedSpec.c_str());
1963+
v8::Local<v8::Value> blobDataVal;
1964+
if (!getDataFn->Call(context, internalAccessor, 1, &urlArg).ToLocal(&blobDataVal) || blobDataVal->IsNullOrUndefined()) {
1965+
if (IsScriptLoadingLogEnabled()) {
1966+
Log(@"[dyn-import][blob] blob not found in BLOB_STORE: %s", normalizedSpec.c_str());
1967+
}
1968+
std::string msg = "Blob not found: " + normalizedSpec;
1969+
resolver->Reject(context, v8::Exception::Error(tns::ToV8String(isolate, msg.c_str()))).FromMaybe(false);
1970+
return scope.Escape(resolver->GetPromise());
1971+
}
1972+
1973+
// blobDataVal should be {blob: Blob, type: string, ext: string}
1974+
// We need to get the text from the Blob
1975+
if (!blobDataVal->IsObject()) {
1976+
if (IsScriptLoadingLogEnabled()) {
1977+
Log(@"[dyn-import][blob] blob data is not an object");
1978+
}
1979+
resolver->Reject(context, v8::Exception::Error(tns::ToV8String(isolate, "Invalid blob data"))).FromMaybe(false);
1980+
return scope.Escape(resolver->GetPromise());
1981+
}
1982+
v8::Local<v8::Object> blobData = blobDataVal.As<v8::Object>();
1983+
1984+
// Get the actual Blob object
1985+
v8::Local<v8::Value> blobVal;
1986+
if (!blobData->Get(context, tns::ToV8String(isolate, "blob")).ToLocal(&blobVal) || !blobVal->IsObject()) {
1987+
if (IsScriptLoadingLogEnabled()) {
1988+
Log(@"[dyn-import][blob] blob property not found");
1989+
}
1990+
resolver->Reject(context, v8::Exception::Error(tns::ToV8String(isolate, "Blob object not found"))).FromMaybe(false);
1991+
return scope.Escape(resolver->GetPromise());
1992+
}
1993+
v8::Local<v8::Object> blobObj = blobVal.As<v8::Object>();
1994+
1995+
// Call blob.text() to get the source code as a Promise
1996+
v8::Local<v8::Value> textFnVal;
1997+
if (!blobObj->Get(context, tns::ToV8String(isolate, "text")).ToLocal(&textFnVal) || !textFnVal->IsFunction()) {
1998+
if (IsScriptLoadingLogEnabled()) {
1999+
Log(@"[dyn-import][blob] Blob.text() not available");
2000+
}
2001+
resolver->Reject(context, v8::Exception::Error(tns::ToV8String(isolate, "Blob.text() not available"))).FromMaybe(false);
2002+
return scope.Escape(resolver->GetPromise());
2003+
}
2004+
v8::Local<v8::Function> textFn = textFnVal.As<v8::Function>();
2005+
2006+
v8::Local<v8::Value> textPromiseVal;
2007+
if (!textFn->Call(context, blobObj, 0, nullptr).ToLocal(&textPromiseVal) || !textPromiseVal->IsPromise()) {
2008+
if (IsScriptLoadingLogEnabled()) {
2009+
Log(@"[dyn-import][blob] Blob.text() did not return a Promise");
2010+
}
2011+
resolver->Reject(context, v8::Exception::Error(tns::ToV8String(isolate, "Blob.text() failed"))).FromMaybe(false);
2012+
return scope.Escape(resolver->GetPromise());
2013+
}
2014+
v8::Local<v8::Promise> textPromise = textPromiseVal.As<v8::Promise>();
2015+
2016+
// Create data structure to pass to the callbacks
2017+
struct BlobImportData {
2018+
v8::Global<v8::Promise::Resolver> resolver;
2019+
v8::Global<v8::Context> ctx;
2020+
std::string blobUrl;
2021+
};
2022+
auto* data = new BlobImportData{
2023+
v8::Global<v8::Promise::Resolver>(isolate, resolver),
2024+
v8::Global<v8::Context>(isolate, context),
2025+
normalizedSpec
2026+
};
2027+
2028+
// Success callback: compile and execute the module
2029+
auto onFulfilled = [](const v8::FunctionCallbackInfo<v8::Value>& info) {
2030+
v8::Isolate* iso = info.GetIsolate();
2031+
v8::HandleScope hs(iso);
2032+
if (!info.Data()->IsExternal()) return;
2033+
auto* d = static_cast<BlobImportData*>(info.Data().As<v8::External>()->Value());
2034+
v8::Local<v8::Context> ctx = d->ctx.Get(iso);
2035+
v8::Local<v8::Promise::Resolver> res = d->resolver.Get(iso);
2036+
2037+
if (info.Length() < 1 || !info[0]->IsString()) {
2038+
res->Reject(ctx, v8::Exception::Error(tns::ToV8String(iso, "Blob text is not a string"))).FromMaybe(false);
2039+
delete d;
2040+
return;
2041+
}
2042+
2043+
v8::String::Utf8Value codeUtf8(iso, info[0]);
2044+
std::string code = *codeUtf8 ? *codeUtf8 : "";
2045+
2046+
if (IsScriptLoadingLogEnabled()) {
2047+
Log(@"[dyn-import][blob] compiling blob module, code length=%zu", code.size());
2048+
}
2049+
2050+
// Compile and execute the module
2051+
v8::MaybeLocal<v8::Module> modMaybe = CompileModuleFromSource(iso, ctx, code, d->blobUrl);
2052+
v8::Local<v8::Module> mod;
2053+
if (!modMaybe.ToLocal(&mod)) {
2054+
res->Reject(ctx, v8::Exception::Error(tns::ToV8String(iso, "Failed to compile blob module"))).FromMaybe(false);
2055+
delete d;
2056+
return;
2057+
}
2058+
2059+
// Register the module
2060+
g_moduleRegistry[d->blobUrl].Reset(iso, mod);
2061+
2062+
res->Resolve(ctx, mod->GetModuleNamespace()).FromMaybe(false);
2063+
delete d;
2064+
};
2065+
2066+
// Error callback
2067+
auto onRejected = [](const v8::FunctionCallbackInfo<v8::Value>& info) {
2068+
v8::Isolate* iso = info.GetIsolate();
2069+
v8::HandleScope hs(iso);
2070+
if (!info.Data()->IsExternal()) return;
2071+
auto* d = static_cast<BlobImportData*>(info.Data().As<v8::External>()->Value());
2072+
v8::Local<v8::Context> ctx = d->ctx.Get(iso);
2073+
v8::Local<v8::Promise::Resolver> res = d->resolver.Get(iso);
2074+
v8::Local<v8::Value> reason = info.Length() > 0 ? info[0] : v8::Exception::Error(tns::ToV8String(iso, "Blob text() failed"));
2075+
res->Reject(ctx, reason).FromMaybe(false);
2076+
delete d;
2077+
};
2078+
2079+
v8::Local<v8::Function> onFulfilledFn = v8::Function::New(context, onFulfilled, v8::External::New(isolate, data)).ToLocalChecked();
2080+
v8::Local<v8::Function> onRejectedFn = v8::Function::New(context, onRejected, v8::External::New(isolate, data)).ToLocalChecked();
2081+
2082+
textPromise->Then(context, onFulfilledFn, onRejectedFn).FromMaybe(v8::Local<v8::Promise>());
2083+
2084+
return scope.Escape(resolver->GetPromise());
2085+
}
2086+
19152087
// If spec is an HTTP(S) URL, try HTTP fetch+compile directly
19162088
if (!normalizedSpec.empty() && (StartsWith(normalizedSpec, "http://") || StartsWith(normalizedSpec, "https://"))) {
19172089
if (IsScriptLoadingLogEnabled()) {

0 commit comments

Comments
 (0)