Skip to content

Commit b2b65fa

Browse files
committed
feat: handling external modules
1 parent f72b565 commit b2b65fa

File tree

3 files changed

+217
-3
lines changed

3 files changed

+217
-3
lines changed

NativeScript/runtime/ModuleInternal.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ class ModuleInternal {
3939
const std::string& moduleName);
4040
std::string ResolvePathFromPackageJson(const std::string& packageJson,
4141
bool& error);
42+
v8::Local<v8::Object> CreatePlaceholderModule(v8::Isolate* isolate,
43+
const std::string& moduleName,
44+
const std::string& cacheKey);
4245
static v8::ScriptCompiler::CachedData* LoadScriptCache(
4346
const std::string& path);
4447
static void SaveScriptCache(const v8::Local<v8::Script> script,

NativeScript/runtime/ModuleInternal.mm

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,23 @@
88
#include "Helpers.h"
99
#include "ModuleInternalCallbacks.h" // for ResolveModuleCallback
1010
#include "NativeScriptException.h"
11+
#include "Runtime.h" // for GetAppConfigValue
1112
#include "RuntimeConfig.h"
1213

1314
using namespace v8;
1415

1516
namespace tns {
1617

18+
// Helper function to check if a module name looks like an optional external module
19+
bool IsLikelyOptionalModule(const std::string& moduleName) {
20+
// Check if it's a bare module name (no path separators) that could be an npm package
21+
if (moduleName.find('/') == std::string::npos && moduleName.find('\\') == std::string::npos &&
22+
moduleName[0] != '.' && moduleName[0] != '~' && moduleName[0] != '/') {
23+
return true;
24+
}
25+
return false;
26+
}
27+
1728
ModuleInternal::ModuleInternal(Local<Context> context) {
1829
std::string requireFactoryScript = "(function() { "
1930
" function require_factory(requireInternal, dirName) { "
@@ -182,6 +193,10 @@
182193
Local<Value> exportsObj;
183194
std::string path = this->ResolvePath(isolate, baseDir, moduleName);
184195
if (path.empty()) {
196+
// Create placeholder module for optional modules
197+
if (IsLikelyOptionalModule(moduleName)) {
198+
return this->CreatePlaceholderModule(isolate, moduleName, cacheKey);
199+
}
185200
return Local<Object>();
186201
}
187202

@@ -511,6 +526,11 @@ ScriptOrigin origin(
511526
}
512527

513528
if (exists == NO) {
529+
// Check if this looks like an optional module
530+
if (IsLikelyOptionalModule(moduleName)) {
531+
// Return empty string to indicate optional module not found
532+
return std::string();
533+
}
514534
throw NativeScriptException("The specified module does not exist: " + moduleName);
515535
}
516536

@@ -544,6 +564,11 @@ throw NativeScriptException("Unable to locate main entry in " +
544564

545565
exists = [fileManager fileExistsAtPath:fullPath isDirectory:&isDirectory];
546566
if (exists == NO) {
567+
// Check if this looks like an optional module
568+
if (IsLikelyOptionalModule(moduleName)) {
569+
// Return empty string to indicate optional module not found
570+
return std::string();
571+
}
547572
throw NativeScriptException("The specified module does not exist: " + moduleName);
548573
}
549574

@@ -592,6 +617,69 @@ throw NativeScriptException("Unable to locate main entry in " +
592617
return [path UTF8String];
593618
}
594619

620+
Local<Object> ModuleInternal::CreatePlaceholderModule(Isolate* isolate,
621+
const std::string& moduleName,
622+
const std::string& cacheKey) {
623+
Local<Context> context = isolate->GetCurrentContext();
624+
625+
// Create a module object with exports that throws when accessed
626+
Local<Object> moduleObj = Object::New(isolate);
627+
628+
// Create a Proxy that throws an error when any property is accessed
629+
std::string errorMessage =
630+
"Module '" + moduleName + "' is not available. This is an optional module.";
631+
std::string proxyCode = "(function() {"
632+
" const error = new Error('" +
633+
errorMessage +
634+
"');"
635+
" return new Proxy({}, {"
636+
" get: function(target, prop) {"
637+
" throw error;"
638+
" },"
639+
" set: function(target, prop, value) {"
640+
" throw error;"
641+
" },"
642+
" has: function(target, prop) {"
643+
" return false;"
644+
" },"
645+
" ownKeys: function(target) {"
646+
" return [];"
647+
" },"
648+
" getPrototypeOf: function(target) {"
649+
" return null;"
650+
" }"
651+
" });"
652+
"})()";
653+
654+
Local<Script> proxyScript;
655+
if (Script::Compile(context, tns::ToV8String(isolate, proxyCode.c_str())).ToLocal(&proxyScript)) {
656+
Local<Value> proxyObject;
657+
if (proxyScript->Run(context).ToLocal(&proxyObject)) {
658+
// Set the exports to the proxy object
659+
bool success = moduleObj->Set(context, tns::ToV8String(isolate, "exports"), proxyObject)
660+
.FromMaybe(false);
661+
tns::Assert(success, isolate);
662+
}
663+
}
664+
665+
// Set up the module object
666+
bool success = moduleObj
667+
->Set(context, tns::ToV8String(isolate, "id"),
668+
tns::ToV8String(isolate, moduleName.c_str()))
669+
.FromMaybe(false);
670+
tns::Assert(success, isolate);
671+
672+
success =
673+
moduleObj->Set(context, tns::ToV8String(isolate, "loaded"), v8::Boolean::New(isolate, true))
674+
.FromMaybe(false);
675+
tns::Assert(success, isolate);
676+
677+
// Cache the placeholder module
678+
this->loadedModules_[cacheKey] = std::make_shared<Persistent<Object>>(isolate, moduleObj);
679+
680+
return moduleObj;
681+
}
682+
595683
ScriptCompiler::CachedData* ModuleInternal::LoadScriptCache(const std::string& path) {
596684
if (RuntimeConfig.IsDebug) {
597685
return nullptr;

NativeScript/runtime/ModuleInternalCallbacks.mm

Lines changed: 126 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,26 @@ static bool IsScriptLoadingLogEnabled() {
2727
return value ? [value boolValue] : false;
2828
}
2929

30+
// Helper function to check if a module name looks like an optional external module
31+
static bool IsLikelyOptionalModule(const std::string& moduleName) {
32+
// Skip Node.js built-in modules (they should be handled separately)
33+
if (moduleName.rfind("node:", 0) == 0) {
34+
return false;
35+
}
36+
37+
// Check if it's a bare module name (no path separators) that could be an npm package
38+
if (moduleName.find('/') == std::string::npos && moduleName.find('\\') == std::string::npos &&
39+
moduleName[0] != '.' && moduleName[0] != '~' && moduleName[0] != '/') {
40+
return true;
41+
}
42+
return false;
43+
}
44+
45+
// Helper function to check if a module name is a Node.js built-in module
46+
static bool IsNodeBuiltinModule(const std::string& moduleName) {
47+
return moduleName.rfind("node:", 0) == 0;
48+
}
49+
3050
// ────────────────────────────────────────────────────────────────────────────
3151
// Simple in-process registry: maps absolute file paths → compiled Module handles
3252
std::unordered_map<std::string, v8::Global<v8::Module>> g_moduleRegistry;
@@ -196,9 +216,112 @@ static bool IsScriptLoadingLogEnabled() {
196216
// If we still didn’t resolve to an actual file, surface an exception instead
197217
// of letting ReadModule() assert while trying to open a directory.
198218
if (!isFile(absPath)) {
199-
std::string msg = "Cannot find module " + spec + " (tried " + absPath + ")";
200-
isolate->ThrowException(v8::Exception::Error(tns::ToV8String(isolate, msg)));
201-
return v8::MaybeLocal<v8::Module>();
219+
// Check if this is a Node.js built-in module (e.g., node:url)
220+
if (IsNodeBuiltinModule(spec)) {
221+
// Strip the "node:" prefix and try to resolve as a regular module
222+
std::string builtinName = spec.substr(5); // Remove "node:" prefix
223+
std::string builtinPath = RuntimeConfig.ApplicationPath + "/" + builtinName + ".mjs";
224+
225+
// Check if a polyfill file exists
226+
if (!isFile(builtinPath)) {
227+
// Create a basic polyfill for the built-in module
228+
std::string polyfillContent;
229+
230+
if (builtinName == "url") {
231+
// Create a polyfill for node:url with fileURLToPath
232+
polyfillContent = "// Polyfill for node:url\n"
233+
"export function fileURLToPath(url) {\n"
234+
" if (typeof url === 'string') {\n"
235+
" if (url.startsWith('file://')) {\n"
236+
" return decodeURIComponent(url.slice(7));\n"
237+
" }\n"
238+
" return url;\n"
239+
" }\n"
240+
" if (url && typeof url.href === 'string') {\n"
241+
" return fileURLToPath(url.href);\n"
242+
" }\n"
243+
" throw new Error('Invalid URL');\n"
244+
"}\n"
245+
"\n"
246+
"export function pathToFileURL(path) {\n"
247+
" return new URL('file://' + encodeURIComponent(path));\n"
248+
"}\n";
249+
} else {
250+
// Generic polyfill for other Node.js built-in modules
251+
polyfillContent = "// Polyfill for node:" + builtinName +
252+
"\n"
253+
"console.warn('Node.js built-in module \\'node:" +
254+
builtinName +
255+
"\\' is not fully supported in NativeScript');\n"
256+
"export default {};\n";
257+
}
258+
259+
// Write polyfill file
260+
NSString* polyfillPathStr = [NSString stringWithUTF8String:builtinPath.c_str()];
261+
NSString* polyfillContentStr = [NSString stringWithUTF8String:polyfillContent.c_str()];
262+
263+
if ([polyfillContentStr writeToFile:polyfillPathStr
264+
atomically:YES
265+
encoding:NSUTF8StringEncoding
266+
error:nil]) {
267+
// File created successfully, now resolve it normally
268+
absPath = builtinPath;
269+
} else {
270+
// Failed to create file, fall back to throwing error
271+
std::string msg = "Cannot find module " + spec + " (tried " + absPath + ")";
272+
isolate->ThrowException(v8::Exception::Error(tns::ToV8String(isolate, msg)));
273+
return v8::MaybeLocal<v8::Module>();
274+
}
275+
} else {
276+
// Polyfill file already exists, use it
277+
absPath = builtinPath;
278+
}
279+
} else if (IsLikelyOptionalModule(spec)) {
280+
// Create a placeholder file on disk that webpack can resolve to
281+
std::string appPath = RuntimeConfig.ApplicationPath;
282+
std::string placeholderPath = appPath + "/" + spec + ".mjs";
283+
284+
// Check if placeholder file already exists
285+
if (!isFile(placeholderPath)) {
286+
// Create placeholder content
287+
std::string placeholderContent = "const error = new Error('Module \\'" + spec +
288+
"\\' is not available. This is an optional module.');\n"
289+
"const proxy = new Proxy({}, {\n"
290+
" get: function(target, prop) { throw error; },\n"
291+
" set: function(target, prop, value) { throw error; },\n"
292+
" has: function(target, prop) { return false; },\n"
293+
" ownKeys: function(target) { return []; },\n"
294+
" getPrototypeOf: function(target) { return null; }\n"
295+
"});\n"
296+
"export default proxy;\n";
297+
298+
// Write placeholder file
299+
NSString* placeholderPathStr = [NSString stringWithUTF8String:placeholderPath.c_str()];
300+
NSString* placeholderContentStr =
301+
[NSString stringWithUTF8String:placeholderContent.c_str()];
302+
303+
if ([placeholderContentStr writeToFile:placeholderPathStr
304+
atomically:YES
305+
encoding:NSUTF8StringEncoding
306+
error:nil]) {
307+
// File created successfully, now resolve it normally
308+
absPath = placeholderPath;
309+
} else {
310+
// Failed to create file, fall back to throwing error
311+
std::string msg = "Cannot find module " + spec + " (tried " + absPath + ")";
312+
isolate->ThrowException(v8::Exception::Error(tns::ToV8String(isolate, msg)));
313+
return v8::MaybeLocal<v8::Module>();
314+
}
315+
} else {
316+
// Placeholder file already exists, use it
317+
absPath = placeholderPath;
318+
}
319+
} else {
320+
// Not an optional module, throw the original error
321+
std::string msg = "Cannot find module " + spec + " (tried " + absPath + ")";
322+
isolate->ThrowException(v8::Exception::Error(tns::ToV8String(isolate, msg)));
323+
return v8::MaybeLocal<v8::Module>();
324+
}
202325
}
203326

204327
// Special handling for JSON imports (e.g. import data from './foo.json' assert {type:'json'})

0 commit comments

Comments
 (0)