Skip to content

Commit b7ac75c

Browse files
committed
feat: dynamic import support for lazy loading
1 parent 485570c commit b7ac75c

File tree

2 files changed

+148
-20
lines changed

2 files changed

+148
-20
lines changed

NativeScript/runtime/ModuleInternal.mm

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -320,14 +320,14 @@ throw NativeScriptException(isolate,
320320
}
321321

322322
Local<Value> ModuleInternal::LoadScript(Isolate* isolate, const std::string& path) {
323-
// Simple dispatch on extension:
324323
if (path.size() >= 4 && path.compare(path.size() - 4, 4, ".mjs") == 0) {
324+
// Treat all .mjs files as standard ES modules.
325325
return ModuleInternal::LoadESModule(isolate, path);
326-
} else {
327-
Local<Script> script = ModuleInternal::LoadClassicScript(isolate, path);
328-
// run it and return the value
329-
return script->Run(isolate->GetCurrentContext()).ToLocalChecked();
330326
}
327+
328+
Local<Script> script = ModuleInternal::LoadClassicScript(isolate, path);
329+
// run it and return the value
330+
return script->Run(isolate->GetCurrentContext()).ToLocalChecked();
331331
}
332332

333333
Local<Script> ModuleInternal::LoadClassicScript(Isolate* isolate, const std::string& path) {

NativeScript/runtime/ModuleInternalCallbacks.mm

Lines changed: 143 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include "Helpers.h" // for tns::Exists
99
#include "ModuleInternal.h" // for LoadScript(...)
1010
#include "NativeScriptException.h"
11+
#include "Runtime.h" // for GetAppConfigValue
1112
#include "RuntimeConfig.h"
1213

1314
// Do NOT pull all v8 symbols into namespace here; String would clash with
@@ -16,6 +17,16 @@
1617

1718
namespace tns {
1819

20+
// ────────────────────────────────────────────────────────────────────────────
21+
// Helper function to check if script loading logging is enabled
22+
// This reads the "logScriptLoading" boolean option from nativescript.config (aka, package.json in
23+
// the app bundle). Usage: Add "logScriptLoading": true to your nativescript.config to enable
24+
// verbose logging of module resolution and dynamic imports for debugging.
25+
static bool IsScriptLoadingLogEnabled() {
26+
id value = Runtime::GetAppConfigValue("logScriptLoading");
27+
return value ? [value boolValue] : false;
28+
}
29+
1930
// ────────────────────────────────────────────────────────────────────────────
2031
// Simple in-process registry: maps absolute file paths → compiled Module handles
2132
std::unordered_map<std::string, v8::Global<v8::Module>> g_moduleRegistry;
@@ -43,8 +54,14 @@
4354
break;
4455
}
4556
}
46-
if (referrerPath.empty()) {
47-
// we never compiled this referrer
57+
// If we couldn't identify the referrer (e.g. coming from a dynamic import
58+
// where the embedder did not pass the compiled Module), we can still proceed
59+
// for absolute and application-rooted specifiers. Only bail out early when
60+
// the specifier is clearly relative (starts with "./" or "../") and we
61+
// would need the referrer's directory to resolve it.
62+
bool specIsRelative = !spec.empty() && spec[0] == '.';
63+
if (referrerPath.empty() && specIsRelative) {
64+
// Unable to resolve a relative path without knowing the base directory.
4865
return v8::MaybeLocal<v8::Module>();
4966
}
5067

@@ -76,11 +93,17 @@
7693
}
7794
// If starts with /app/... drop the leading /app
7895
const std::string appPrefix = "/app/";
96+
std::string tailNoApp = tail;
7997
if (tail.rfind(appPrefix, 0) == 0) {
80-
tail = tail.substr(appPrefix.size());
98+
tailNoApp = tail.substr(appPrefix.size());
8199
}
82-
std::string base = RuntimeConfig.ApplicationPath + "/" + tail;
83-
candidateBases.push_back(base);
100+
// Candidate that keeps /app/ prefix stripped
101+
std::string baseNoApp = RuntimeConfig.ApplicationPath + "/" + tailNoApp;
102+
candidateBases.push_back(baseNoApp);
103+
104+
// Also try path with original tail (includes /app/...) directly under application dir
105+
std::string baseWithApp = RuntimeConfig.ApplicationPath + tail; // tail already begins with '/'
106+
candidateBases.push_back(baseWithApp);
84107
} else if (!spec.empty() && spec[0] == '~') {
85108
// Alias to application root using ~/path
86109
std::string tail = spec.size() >= 2 && spec[1] == '/' ? spec.substr(2) : spec.substr(1);
@@ -130,7 +153,12 @@
130153
for (const std::string& baseCandidate : candidateBases) {
131154
absPath = baseCandidate;
132155

133-
if (!isFile(absPath)) {
156+
bool existsNow = isFile(absPath);
157+
if (IsScriptLoadingLogEnabled()) {
158+
NSLog(@"[resolver] %s -> %s", absPath.c_str(), existsNow ? "file" : "missing");
159+
}
160+
161+
if (!existsNow) {
134162
// 1) Try adding .mjs, .js
135163
const char* exts[] = {".mjs", ".js"};
136164
bool found = false;
@@ -244,29 +272,39 @@
244272
v8::Local<v8::Context> context, v8::Local<v8::ScriptOrModule> referrer,
245273
v8::Local<v8::String> specifier, v8::Local<v8::FixedArray> import_assertions) {
246274
v8::Isolate* isolate = context->GetIsolate();
275+
// Diagnostic: log every dynamic import attempt.
276+
v8::String::Utf8Value specUtf8(isolate, specifier);
277+
const char* cSpec = (*specUtf8) ? *specUtf8 : "<invalid>";
278+
NSString* specStr = [NSString stringWithUTF8String:cSpec];
279+
if (IsScriptLoadingLogEnabled()) {
280+
NSLog(@"[dyn-import] → %@", specStr);
281+
}
247282
v8::EscapableHandleScope scope(isolate);
248283

249284
// Create a Promise resolver we'll resolve/reject synchronously for now.
250285
v8::Local<v8::Promise::Resolver> resolver = v8::Promise::Resolver::New(context).ToLocalChecked();
251286

252287
// Re-use the static resolver to locate / compile the module.
253288
try {
254-
v8::Local<v8::Module> refMod; // empty -> ResolveModuleCallback falls back to absPath logic
289+
// Pass empty referrer since this V8 version doesn't expose GetModule() on
290+
// ScriptOrModule. The resolver will fall back to absolute-path heuristics.
291+
v8::Local<v8::Module> refMod;
292+
255293
v8::MaybeLocal<v8::Module> maybeModule =
256294
ResolveModuleCallback(context, specifier, import_assertions, refMod);
257295

258296
v8::Local<v8::Module> module;
259297
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();
298+
// ResolveModuleCallback already threw; forward the V8 exception
264299
return scope.Escape(resolver->GetPromise());
265300
}
266301

267302
// If not yet instantiated/evaluated, do it now
268303
if (module->GetStatus() == v8::Module::kUninstantiated) {
269304
if (!module->InstantiateModule(context, &ResolveModuleCallback).FromMaybe(false)) {
305+
if (IsScriptLoadingLogEnabled()) {
306+
NSLog(@"[dyn-import] ✗ instantiate failed %@", specStr);
307+
}
270308
resolver
271309
->Reject(context,
272310
v8::Exception::Error(tns::ToV8String(isolate, "Failed to instantiate module")))
@@ -277,17 +315,107 @@
277315

278316
if (module->GetStatus() != v8::Module::kEvaluated) {
279317
if (module->Evaluate(context).IsEmpty()) {
280-
resolver
281-
->Reject(context,
282-
v8::Exception::Error(tns::ToV8String(isolate, "Failed to evaluate module")))
283-
.Check();
318+
if (IsScriptLoadingLogEnabled()) {
319+
NSLog(@"[dyn-import] ✗ evaluation failed %@", specStr);
320+
}
321+
v8::Local<v8::Value> ex =
322+
v8::Exception::Error(tns::ToV8String(isolate, "Evaluation failed"));
323+
resolver->Reject(context, ex).Check();
284324
return scope.Escape(resolver->GetPromise());
285325
}
286326
}
287327

328+
// Special handling for webpack chunks: check if this is a webpack chunk and install it
329+
v8::Local<v8::Value> namespaceObj = module->GetModuleNamespace();
330+
if (namespaceObj->IsObject()) {
331+
v8::Local<v8::Object> nsObj = namespaceObj.As<v8::Object>();
332+
333+
// Check if this is a webpack chunk (has __webpack_ids__ export)
334+
v8::Local<v8::String> webpackIdsKey = tns::ToV8String(isolate, "__webpack_ids__");
335+
v8::Local<v8::Value> webpackIds;
336+
if (nsObj->Get(context, webpackIdsKey).ToLocal(&webpackIds) && !webpackIds->IsUndefined()) {
337+
if (IsScriptLoadingLogEnabled()) {
338+
NSLog(@"[dyn-import] Detected webpack chunk %@", specStr);
339+
}
340+
// This is a webpack chunk, get the webpack runtime from the runtime module
341+
try {
342+
// Import the runtime module to get __webpack_require__
343+
// For import assertions, we need to pass an empty FixedArray
344+
// Use the empty fixed array from the isolate's roots
345+
v8::Local<v8::FixedArray> empty_assertions = v8::Local<v8::FixedArray>();
346+
v8::MaybeLocal<v8::Module> maybeRuntimeModule =
347+
ResolveModuleCallback(context, tns::ToV8String(isolate, "file:///app/runtime.mjs"),
348+
empty_assertions, v8::Local<v8::Module>());
349+
350+
v8::Local<v8::Module> runtimeModule;
351+
if (maybeRuntimeModule.ToLocal(&runtimeModule)) {
352+
v8::Local<v8::Value> runtimeNamespace = runtimeModule->GetModuleNamespace();
353+
if (runtimeNamespace->IsObject()) {
354+
v8::Local<v8::Object> runtimeObj = runtimeNamespace.As<v8::Object>();
355+
v8::Local<v8::String> defaultKey = tns::ToV8String(isolate, "default");
356+
v8::Local<v8::Value> webpackRequire;
357+
358+
if (runtimeObj->Get(context, defaultKey).ToLocal(&webpackRequire) &&
359+
webpackRequire->IsObject()) {
360+
if (IsScriptLoadingLogEnabled()) {
361+
NSLog(@"[dyn-import] Found runtime module default export");
362+
}
363+
v8::Local<v8::String> installKey = tns::ToV8String(isolate, "C");
364+
v8::Local<v8::Value> installFn;
365+
if (webpackRequire.As<v8::Object>()->Get(context, installKey).ToLocal(&installFn) &&
366+
installFn->IsFunction()) {
367+
if (IsScriptLoadingLogEnabled()) {
368+
NSLog(@"[dyn-import] Calling webpack installChunk function");
369+
}
370+
// Call webpack's installChunk function with the module namespace
371+
v8::Local<v8::Value> args[] = {namespaceObj};
372+
v8::Local<v8::Value> result;
373+
if (!installFn.As<v8::Function>()
374+
->Call(context, v8::Undefined(isolate), 1, args)
375+
.ToLocal(&result)) {
376+
// If the call fails, we can ignore it since this is just a helper for webpack
377+
// chunks
378+
if (IsScriptLoadingLogEnabled()) {
379+
NSLog(@"[dyn-import] ✗ webpack installChunk call failed");
380+
}
381+
} else {
382+
if (IsScriptLoadingLogEnabled()) {
383+
NSLog(@"[dyn-import] ✓ webpack installChunk call succeeded");
384+
}
385+
}
386+
} else {
387+
if (IsScriptLoadingLogEnabled()) {
388+
NSLog(@"[dyn-import] ✗ webpack installChunk function not found");
389+
}
390+
}
391+
} else {
392+
if (IsScriptLoadingLogEnabled()) {
393+
NSLog(@"[dyn-import] ✗ runtime module default export not found");
394+
}
395+
}
396+
}
397+
} else {
398+
if (IsScriptLoadingLogEnabled()) {
399+
NSLog(@"[dyn-import] ✗ runtime module not found");
400+
}
401+
}
402+
} catch (...) {
403+
if (IsScriptLoadingLogEnabled()) {
404+
NSLog(@"[dyn-import] ✗ exception while accessing runtime module");
405+
}
406+
}
407+
}
408+
}
409+
288410
resolver->Resolve(context, module->GetModuleNamespace()).Check();
411+
if (IsScriptLoadingLogEnabled()) {
412+
NSLog(@"[dyn-import] ✓ resolved %@", specStr);
413+
}
289414
} catch (NativeScriptException& ex) {
290415
ex.ReThrowToV8(isolate);
416+
if (IsScriptLoadingLogEnabled()) {
417+
NSLog(@"[dyn-import] ✗ native failed %@", specStr);
418+
}
291419
resolver
292420
->Reject(context, v8::Exception::Error(
293421
tns::ToV8String(isolate, "Native error during dynamic import")))

0 commit comments

Comments
 (0)