Skip to content

Commit 485570c

Browse files
committed
feat: dynamic import, lazy loaded chunks wip
1 parent f42605c commit 485570c

File tree

3 files changed

+100
-12
lines changed

3 files changed

+100
-12
lines changed

NativeScript/runtime/ModuleInternalCallbacks.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,10 @@ v8::MaybeLocal<v8::Module> ResolveModuleCallback(
1616
v8::Local<v8::FixedArray> import_assertions,
1717
v8::Local<v8::Module> referrer);
1818

19+
// Host callback for dynamic import() expressions
20+
v8::MaybeLocal<v8::Promise> ImportModuleDynamicallyCallback(
21+
v8::Local<v8::Context> context, v8::Local<v8::ScriptOrModule> referrer,
22+
v8::Local<v8::String> specifier,
23+
v8::Local<v8::FixedArray> import_assertions);
24+
1925
} // namespace tns

NativeScript/runtime/ModuleInternalCallbacks.mm

Lines changed: 86 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#include "ModuleInternalCallbacks.h"
33
#include <sys/stat.h>
44
#include <v8.h>
5+
#include <queue>
56
#include <string>
67
#include <unordered_map>
78
#include "Helpers.h" // for tns::Exists
@@ -67,26 +68,41 @@
6768
// Relative import (./ or ../)
6869
std::string cleanSpec = spec.rfind("./", 0) == 0 ? spec.substr(2) : spec;
6970
candidateBases.push_back(baseDir + cleanSpec);
71+
} else if (spec.rfind("file://", 0) == 0) {
72+
// Absolute file URL, e.g. file:///app/path/to/chunk.mjs
73+
std::string tail = spec.substr(7); // strip file://
74+
if (tail.rfind("/", 0) != 0) {
75+
tail = "/" + tail;
76+
}
77+
// If starts with /app/... drop the leading /app
78+
const std::string appPrefix = "/app/";
79+
if (tail.rfind(appPrefix, 0) == 0) {
80+
tail = tail.substr(appPrefix.size());
81+
}
82+
std::string base = RuntimeConfig.ApplicationPath + "/" + tail;
83+
candidateBases.push_back(base);
7084
} else if (!spec.empty() && spec[0] == '~') {
71-
// App root alias "~/" → <ApplicationPath>/
85+
// Alias to application root using ~/path
7286
std::string tail = spec.size() >= 2 && spec[1] == '/' ? spec.substr(2) : spec.substr(1);
7387
std::string base = RuntimeConfig.ApplicationPath + "/" + tail;
7488
candidateBases.push_back(base);
7589
} else if (!spec.empty() && spec[0] == '/') {
7690
// Absolute path within the bundle
7791
candidateBases.push_back(spec);
7892
} else {
79-
// Bare specifier – look inside tns_modules like the CommonJS resolver
80-
NSString* tnsModulesPath =
81-
[[NSString stringWithUTF8String:RuntimeConfig.ApplicationPath.c_str()]
82-
stringByAppendingPathComponent:@"tns_modules"];
83-
84-
std::string base1 = std::string([tnsModulesPath UTF8String]) + "/" + spec;
85-
candidateBases.push_back(base1);
93+
// Bare specifier – resolve relative to the application root directory
94+
std::string base = RuntimeConfig.ApplicationPath + "/" + spec;
95+
candidateBases.push_back(base);
8696

87-
// Fallback to tns-core-modules/<spec>
88-
std::string base2 = base1 + "/tns-core-modules/" + spec;
89-
candidateBases.push_back(base2);
97+
// Additional heuristic: Webpack encodes path separators as underscores in
98+
// chunk IDs (e.g. "src_app_components_foo_bar_ts.mjs"). Try converting
99+
// those underscores back to slashes and look for that file as well.
100+
std::string withSlashes = spec;
101+
std::replace(withSlashes.begin(), withSlashes.end(), '_', '/');
102+
std::string baseSlashes = RuntimeConfig.ApplicationPath + "/" + withSlashes;
103+
if (baseSlashes != base) {
104+
candidateBases.push_back(baseSlashes);
105+
}
90106
}
91107

92108
// We'll iterate these bases and attempt to resolve to an actual file
@@ -221,4 +237,63 @@
221237
}
222238
return v8::MaybeLocal<v8::Module>(it2->second.Get(isolate));
223239
}
240+
241+
// ────────────────────────────────────────────────────────────────────────────
242+
// Dynamic import() host callback
243+
v8::MaybeLocal<v8::Promise> ImportModuleDynamicallyCallback(
244+
v8::Local<v8::Context> context, v8::Local<v8::ScriptOrModule> referrer,
245+
v8::Local<v8::String> specifier, v8::Local<v8::FixedArray> import_assertions) {
246+
v8::Isolate* isolate = context->GetIsolate();
247+
v8::EscapableHandleScope scope(isolate);
248+
249+
// Create a Promise resolver we'll resolve/reject synchronously for now.
250+
v8::Local<v8::Promise::Resolver> resolver = v8::Promise::Resolver::New(context).ToLocalChecked();
251+
252+
// Re-use the static resolver to locate / compile the module.
253+
try {
254+
v8::Local<v8::Module> refMod; // empty -> ResolveModuleCallback falls back to absPath logic
255+
v8::MaybeLocal<v8::Module> maybeModule =
256+
ResolveModuleCallback(context, specifier, import_assertions, refMod);
257+
258+
v8::Local<v8::Module> module;
259+
if (!maybeModule.ToLocal(&module)) {
260+
// resolution failed → reject
261+
std::string specStr = tns::ToString(isolate, specifier);
262+
std::string errMsg = "Cannot resolve module " + specStr;
263+
resolver->Reject(context, v8::Exception::Error(tns::ToV8String(isolate, errMsg))).Check();
264+
return scope.Escape(resolver->GetPromise());
265+
}
266+
267+
// If not yet instantiated/evaluated, do it now
268+
if (module->GetStatus() == v8::Module::kUninstantiated) {
269+
if (!module->InstantiateModule(context, &ResolveModuleCallback).FromMaybe(false)) {
270+
resolver
271+
->Reject(context,
272+
v8::Exception::Error(tns::ToV8String(isolate, "Failed to instantiate module")))
273+
.Check();
274+
return scope.Escape(resolver->GetPromise());
275+
}
276+
}
277+
278+
if (module->GetStatus() != v8::Module::kEvaluated) {
279+
if (module->Evaluate(context).IsEmpty()) {
280+
resolver
281+
->Reject(context,
282+
v8::Exception::Error(tns::ToV8String(isolate, "Failed to evaluate module")))
283+
.Check();
284+
return scope.Escape(resolver->GetPromise());
285+
}
286+
}
287+
288+
resolver->Resolve(context, module->GetModuleNamespace()).Check();
289+
} catch (NativeScriptException& ex) {
290+
ex.ReThrowToV8(isolate);
291+
resolver
292+
->Reject(context, v8::Exception::Error(
293+
tns::ToV8String(isolate, "Native error during dynamic import")))
294+
.Check();
295+
}
296+
297+
return scope.Escape(resolver->GetPromise());
298+
}
224299
}

NativeScript/runtime/Runtime.mm

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@
2323
#include "IsolateWrapper.h"
2424

2525
#include "ModuleBinding.hpp"
26+
#include "ModuleInternalCallbacks.h"
2627
#include "URLImpl.h"
27-
#include "URLSearchParamsImpl.h"
2828
#include "URLPatternImpl.h"
29+
#include "URLSearchParamsImpl.h"
2930

3031
#define STRINGIZE(x) #x
3132
#define STRINGIZE_VALUE_OF(x) STRINGIZE(x)
@@ -198,6 +199,12 @@ void DisposeIsolateWhenPossible(Isolate* isolate) {
198199
MetadataBuilder::RegisterConstantsOnGlobalObject(isolate, globalTemplate, isWorker);
199200

200201
isolate->SetCaptureStackTraceForUncaughtExceptions(true, 100, StackTrace::kOverview);
202+
203+
// Enable dynamic import() support (handle API rename across V8 versions)
204+
#pragma clang diagnostic push
205+
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
206+
isolate->SetHostImportModuleDynamicallyCallback(tns::ImportModuleDynamicallyCallback);
207+
#pragma clang diagnostic pop
201208
isolate->AddMessageListener(NativeScriptException::OnUncaughtError);
202209

203210
Local<Context> context = Context::New(isolate, nullptr, globalTemplate);

0 commit comments

Comments
 (0)