Skip to content

Commit 7309efc

Browse files
committed
fix: node built-in modules handling
1 parent 8d2c6c6 commit 7309efc

File tree

3 files changed

+136
-104
lines changed

3 files changed

+136
-104
lines changed

NativeScript/runtime/ModuleInternalCallbacks.mm

Lines changed: 98 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1240,117 +1240,111 @@ static bool IsDocumentsPath(const std::string& path) {
12401240

12411241
// Check if this is a Node.js built-in module (e.g., node:url)
12421242
if (IsNodeBuiltinModule(spec)) {
1243-
// Strip the "node:" prefix and try to resolve as a regular module
1243+
// Strip the "node:" prefix and create an in-memory polyfill module.
12441244
std::string builtinName = spec.substr(5); // Remove "node:" prefix
1245-
std::string builtinPath = NormalizePath(RuntimeConfig.ApplicationPath + "/" + builtinName + ".mjs");
1246-
1247-
// Check if a polyfill file exists
1248-
if (!isFile(builtinPath)) {
1249-
// Create a basic polyfill for the built-in module
1250-
std::string polyfillContent;
1251-
1252-
if (builtinName == "url") {
1253-
// Create a polyfill for node:url with fileURLToPath
1254-
polyfillContent = "// Polyfill for node:url\n"
1255-
"export function fileURLToPath(url) {\n"
1256-
" if (typeof url === 'string') {\n"
1257-
" if (url.startsWith('file://')) {\n"
1258-
" return decodeURIComponent(url.slice(7));\n"
1259-
" }\n"
1260-
" return url;\n"
1261-
" }\n"
1262-
" if (url && typeof url.href === 'string') {\n"
1263-
" return fileURLToPath(url.href);\n"
1264-
" }\n"
1265-
" throw new Error('Invalid URL');\n"
1266-
"}\n"
1267-
"\n"
1268-
"export function pathToFileURL(path) {\n"
1269-
" return new URL('file://' + encodeURIComponent(path));\n"
1270-
"}\n";
1271-
} else {
1272-
// Generic polyfill for other Node.js built-in modules
1273-
polyfillContent = "// Polyfill for node:" + builtinName +
1274-
"\n"
1275-
"console.warn('Node.js built-in module \\'node:" +
1276-
builtinName +
1277-
"\\' is not fully supported in NativeScript');\n"
1278-
"export default {};\n";
1279-
}
1280-
1281-
// Write polyfill file
1282-
NSString* polyfillPathStr = [NSString stringWithUTF8String:builtinPath.c_str()];
1283-
NSString* polyfillContentStr = [NSString stringWithUTF8String:polyfillContent.c_str()];
1284-
1285-
if ([polyfillContentStr writeToFile:polyfillPathStr
1286-
atomically:YES
1287-
encoding:NSUTF8StringEncoding
1288-
error:nil]) {
1289-
// File created successfully, now resolve it normally
1290-
absPath = builtinPath;
1291-
} else {
1292-
// Failed to create file, fall back to throwing error
1293-
std::string msg = "Cannot find module " + spec + " (tried " + absPath + ")";
1294-
if (RuntimeConfig.IsDebug) {
1295-
Log(@"Debug mode - Node.js polyfill creation failed: %s", msg.c_str());
1296-
// Return empty instead of crashing in debug mode
1297-
return v8::MaybeLocal<v8::Module>();
1298-
} else {
1299-
isolate->ThrowException(v8::Exception::Error(tns::ToV8String(isolate, msg)));
1300-
return v8::MaybeLocal<v8::Module>();
1301-
}
1245+
1246+
// Use a virtual key for registry
1247+
std::string key = std::string("node:") + builtinName;
1248+
1249+
auto itExisting = g_moduleRegistry.find(key);
1250+
if (itExisting != g_moduleRegistry.end()) {
1251+
v8::Local<v8::Module> existing = itExisting->second.Get(isolate);
1252+
if (!existing.IsEmpty() && existing->GetStatus() != v8::Module::kErrored) {
1253+
return v8::MaybeLocal<v8::Module>(existing);
1254+
}
1255+
RemoveModuleFromRegistry(key);
1256+
}
1257+
1258+
std::string polyfillContent;
1259+
if (builtinName == "url") {
1260+
// Polyfill for node:url with fileURLToPath/pathToFileURL
1261+
polyfillContent =
1262+
"// In-memory polyfill for node:url\n"
1263+
"export function fileURLToPath(url) {\n"
1264+
" if (typeof url === 'string') {\n"
1265+
" if (url.startsWith('file://')) {\n"
1266+
" return decodeURIComponent(url.slice(7));\n"
1267+
" }\n"
1268+
" return url;\n"
1269+
" }\n"
1270+
" if (url && typeof url.href === 'string') {\n"
1271+
" return fileURLToPath(url.href);\n"
1272+
" }\n"
1273+
" throw new Error('Invalid URL');\n"
1274+
"}\n"
1275+
"\n"
1276+
"export function pathToFileURL(path) {\n"
1277+
" const encoded = encodeURIComponent(path).replace(/%2F/g, '/');\n"
1278+
" return new URL('file://' + encoded);\n"
1279+
"}\n";
1280+
} else {
1281+
// Generic polyfill for other Node.js built-in modules
1282+
polyfillContent =
1283+
"// In-memory polyfill for node:" + builtinName + "\n" +
1284+
"console.warn('Node.js built-in module \\'node:" + builtinName +
1285+
"\\' is not fully supported in NativeScript');\n" +
1286+
"export default {};\n";
1287+
}
1288+
1289+
v8::MaybeLocal<v8::Module> m =
1290+
CompileModuleForResolveRegisterOnly(isolate, context, polyfillContent, key);
1291+
if (!m.IsEmpty()) {
1292+
v8::Local<v8::Module> mod;
1293+
if (m.ToLocal(&mod)) {
1294+
return m;
13021295
}
1296+
}
1297+
1298+
std::string msg = "Cannot find module " + spec + " (failed to create in-memory polyfill)";
1299+
if (RuntimeConfig.IsDebug) {
1300+
Log(@"Debug mode - Node.js polyfill creation failed: %s", msg.c_str());
1301+
return v8::MaybeLocal<v8::Module>();
13031302
} else {
1304-
// Polyfill file already exists, use it
1305-
absPath = builtinPath;
1303+
isolate->ThrowException(v8::Exception::Error(tns::ToV8String(isolate, msg)));
1304+
return v8::MaybeLocal<v8::Module>();
13061305
}
13071306
} else if (IsLikelyOptionalModule(spec)) {
1308-
// Treat bare specifiers as optional modules by creating a placeholder ES module that
1309-
// throws on property access. This lets applications guard optional imports at runtime
1310-
// without crashing during startup, especially in development.
1311-
std::string appPath = RuntimeConfig.ApplicationPath;
1312-
std::string placeholderPath = NormalizePath(appPath + "/" + spec + ".mjs");
1313-
1314-
// Check if placeholder file already exists
1315-
if (!isFile(placeholderPath)) {
1316-
// Create placeholder content
1317-
std::string placeholderContent = "const error = new Error('Module \\\'" + spec +
1318-
"\\\' is not available. This is an optional module.');\n"
1319-
"const proxy = new Proxy({}, {\n"
1320-
" get: function(target, prop) { throw error; },\n"
1321-
" set: function(target, prop, value) { throw error; },\n"
1322-
" has: function(target, prop) { return false; },\n"
1323-
" ownKeys: function(target) { return []; },\n"
1324-
" getPrototypeOf: function(target) { return null; }\n"
1325-
"});\n"
1326-
"export default proxy;\n";
1327-
1328-
// Write placeholder file
1329-
NSString* placeholderPathStr = [NSString stringWithUTF8String:placeholderPath.c_str()];
1330-
NSString* placeholderContentStr =
1331-
[NSString stringWithUTF8String:placeholderContent.c_str()];
1332-
1333-
if ([placeholderContentStr writeToFile:placeholderPathStr
1334-
atomically:YES
1335-
encoding:NSUTF8StringEncoding
1336-
error:nil]) {
1337-
// File created successfully, now resolve it normally
1338-
absPath = placeholderPath;
1339-
} else {
1340-
// Failed to create file. In debug, avoid throwing to keep dev sessions alive; in release
1341-
// throw to surface the missing optional module.
1342-
std::string msg = "Cannot find module " + spec + " (tried " + absPath + ")";
1343-
if (RuntimeConfig.IsDebug) {
1344-
Log(@"Debug mode - Optional module placeholder creation failed: %s", msg.c_str());
1345-
return v8::MaybeLocal<v8::Module>();
1346-
} else {
1347-
isolate->ThrowException(v8::Exception::Error(tns::ToV8String(isolate, msg)));
1348-
return v8::MaybeLocal<v8::Module>();
1349-
}
1307+
// Treat bare specifiers as optional modules with an in-memory placeholder ES module
1308+
// that throws on property access. This avoids bundle writes in iOS release builds.
1309+
1310+
std::string key = std::string("optional:") + spec;
1311+
auto itExisting = g_moduleRegistry.find(key);
1312+
if (itExisting != g_moduleRegistry.end()) {
1313+
v8::Local<v8::Module> existing = itExisting->second.Get(isolate);
1314+
if (!existing.IsEmpty() && existing->GetStatus() != v8::Module::kErrored) {
1315+
return v8::MaybeLocal<v8::Module>(existing);
1316+
}
1317+
RemoveModuleFromRegistry(key);
1318+
}
1319+
1320+
std::string placeholderContent =
1321+
"const error = new Error(\"Module '" + spec +
1322+
"' is not available. This is an optional module.\");\n"
1323+
"const proxy = new Proxy({}, {\n"
1324+
" get: function(target, prop) { throw error; },\n"
1325+
" set: function(target, prop, value) { throw error; },\n"
1326+
" has: function(target, prop) { return false; },\n"
1327+
" ownKeys: function(target) { return []; },\n"
1328+
" getPrototypeOf: function(target) { return null; }\n"
1329+
"});\n"
1330+
"export default proxy;\n";
1331+
1332+
v8::MaybeLocal<v8::Module> m =
1333+
CompileModuleForResolveRegisterOnly(isolate, context, placeholderContent, key);
1334+
if (!m.IsEmpty()) {
1335+
v8::Local<v8::Module> mod;
1336+
if (m.ToLocal(&mod)) {
1337+
return m;
13501338
}
1339+
}
1340+
1341+
std::string msg = "Cannot find module " + spec + " (failed to create in-memory optional placeholder)";
1342+
if (RuntimeConfig.IsDebug) {
1343+
Log(@"Debug mode - Optional module placeholder creation failed: %s", msg.c_str());
1344+
return v8::MaybeLocal<v8::Module>();
13511345
} else {
1352-
// Placeholder file already exists, use it
1353-
absPath = placeholderPath;
1346+
isolate->ThrowException(v8::Exception::Error(tns::ToV8String(isolate, msg)));
1347+
return v8::MaybeLocal<v8::Module>();
13541348
}
13551349
} else {
13561350
// Not an optional module, throw the original error
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
describe("Node built-in and optional module resolution", function () {
2+
it("provides an in-memory polyfill for node:url", async function () {
3+
// Dynamic import to exercise ResolveModuleCallback ESM path.
4+
const mod = await import("node:url");
5+
6+
expect(mod).toBeDefined();
7+
expect(typeof mod.fileURLToPath).toBe("function");
8+
expect(typeof mod.pathToFileURL).toBe("function");
9+
10+
const p = mod.fileURLToPath("file:///foo/bar.txt");
11+
expect(p === "/foo/bar.txt" || p === "foo/bar.txt").toBe(true);
12+
13+
const u = mod.pathToFileURL("/foo/bar.txt");
14+
expect(u instanceof URL).toBe(true);
15+
expect(u.protocol).toBe("file:");
16+
});
17+
18+
it("creates an in-memory placeholder for likely-optional modules", async function () {
19+
// Use a name that IsLikelyOptionalModule will treat as optional (no slashes, no extension).
20+
const mod = await import("__ns_optional_test_module__");
21+
22+
expect(mod).toBeDefined();
23+
expect(typeof mod.default).toBe("object");
24+
25+
let threw = false;
26+
try {
27+
// Any property access should throw according to the placeholder implementation.
28+
// eslint-disable-next-line no-unused-expressions
29+
mod.default.someProperty;
30+
} catch (e) {
31+
threw = true;
32+
}
33+
expect(threw).toBe(true);
34+
});
35+
});

TestRunner/app/tests/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ require("./URLPattern");
8787
// HTTP ESM Loader tests
8888
require("./HttpEsmLoaderTests");
8989

90+
// Node built-in and optional module resolution tests (ESM)
91+
require("./NodeBuiltinsAndOptionalModulesTests.mjs");
92+
9093
// Exception handling tests
9194
require("./ExceptionHandlingTests");
9295

0 commit comments

Comments
 (0)