Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
278178f
feat: update Gradle and Android build tools versions to 8.14.3 and 8.…
edusperoni Aug 19, 2025
6b76cad
chore: 9.0.0-alpha.0
NathanWalker Aug 24, 2025
6ab9f01
Merge remote-tracking branch 'origin/main' into feat/allow-conditiona…
NathanWalker Aug 24, 2025
00529a8
feat: es module support
NathanWalker Aug 26, 2025
48d5939
feat: es module support
NathanWalker Aug 26, 2025
fbfb74e
feat: es module support
NathanWalker Aug 27, 2025
2067b0b
feat: sbg support for es modules
NathanWalker Aug 27, 2025
bd24b43
fix: application path to use absolute path instead of relative, more …
NathanWalker Aug 27, 2025
66eff21
feat: handle .js and .mjs extensions when loading main
NathanWalker Aug 27, 2025
25ff64e
chore: es module tests
NathanWalker Aug 27, 2025
74e4097
chore: 9.0.0-alpha.1
NathanWalker Aug 28, 2025
9275d5b
feat: provide node:url polyfill
NathanWalker Aug 28, 2025
1d7e32b
chore: 9.0.0-alpha.2
NathanWalker Aug 28, 2025
bd900f7
feat: use terminal reporter and fix tests
NathanWalker Sep 7, 2025
d25130f
fix: unique filename handling with static binding generator
NathanWalker Sep 7, 2025
e66a428
feat: es module dynamic import support
NathanWalker Sep 7, 2025
ce03b17
feat: logScriptLoading - this may be unnecessary really, added for al…
NathanWalker Sep 7, 2025
ccef166
chore: 9.0.0-alpha.3
NathanWalker Sep 9, 2025
bee756c
Merge remote-tracking branch 'origin/main' into feat/allow-conditiona…
NathanWalker Sep 11, 2025
116a45d
Merge remote-tracking branch 'origin/main' into feat/allow-conditiona…
NathanWalker Oct 23, 2025
2006895
chore: test-app/tools/check_console_test_results.js
NathanWalker Oct 23, 2025
4ce886a
chore: test-app/tools/check_console_test_results.js
NathanWalker Oct 23, 2025
ed5fad9
chore: test-app/tools/check_console_test_results.js
NathanWalker Oct 23, 2025
dd5c2ca
Merge remote-tracking branch 'origin/main' into feat/allow-conditiona…
NathanWalker Nov 5, 2025
7fc7277
chore: cleanup
NathanWalker Nov 5, 2025
282ded6
chore: cleanup
NathanWalker Nov 5, 2025
abaaad8
chore: revert version.h change
NathanWalker Nov 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat: es module support
  • Loading branch information
NathanWalker committed Aug 26, 2025
commit 48d59394e5ec4230752af8cf0b4064c814abe89e
20 changes: 18 additions & 2 deletions test-app/app/src/main/assets/app/test-es-module.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,23 @@ export const message = "Hello from ES Module!";
export function greet(name) {
return `Hello, ${name}!`;
}
export default {

export const moduleType = "ES Module";
export const version = "1.0.0";

// Export object with multiple properties
export const utilities = {
add: (a, b) => a + b,
multiply: (a, b) => a * b,
format: (str) => `[${str}]`
};

// Default export
const defaultExport = {
type: "ESModule",
version: "1.0.0"
version: "1.0.0",
features: ["exports", "imports", "default-export"],
status: "working"
};

export default defaultExport;
151 changes: 89 additions & 62 deletions test-app/app/src/main/assets/app/tests/testESModules.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,102 +8,129 @@ describe("ES Module Tests ", function () {
jasmine.addCustomEqualityTester(myCustomEquality);
});

it("should recognize .mjs files as ES modules", function () {
__log("TEST: Testing ES Module recognition");
it("should load .mjs files as ES modules", function () {
__log("TEST: Loading ES Module (.mjs file)");

// Test that .mjs files are detected as ES modules
var testPath1 = "/app/test-module.mjs";
var testPath2 = "/app/test-module.js";
var testPath3 = "/app/test-module.mjs.map";
var esModuleLoaded = false;
var moduleExports = null;
var errorMessage = "";

// Note: We can't directly call IsESModule from JavaScript, but we can test
// that the module system attempts to load .mjs files

var mjsDetected = true;
try {
// This should attempt to load as ES module (will likely fail since file doesn't exist)
require("./test-es-module.mjs");
// This should load our test ES module
moduleExports = require("./test-es-module.mjs");
esModuleLoaded = true;
__log("ES Module loaded successfully: " + JSON.stringify(moduleExports));
} catch (e) {
// Check if error indicates ES module handling was attempted
mjsDetected = e.message.indexOf("test-es-module.mjs") !== -1;
errorMessage = e.message || e.toString();
__log("Error loading ES module: " + errorMessage);
}

expect(mjsDetected).toBe(true);
expect(esModuleLoaded).toBe(true);
expect(moduleExports).not.toBe(null);
});

it("should not treat .mjs.map files as ES modules", function () {
__log("TEST: Testing source map exclusion");
it("should provide ES module exports through namespace", function () {
__log("TEST: Testing ES module exports");

var hasCorrectExports = false;
var moduleExports = null;

var sourceMapRejected = true;
try {
// This should not be treated as an ES module
require("./non-existent.mjs.map");
moduleExports = require("./test-es-module.mjs");

// Test if we can access named exports through the namespace
var hasMessage = moduleExports.hasOwnProperty('message');
var hasGreet = moduleExports.hasOwnProperty('greet');
var hasDefault = moduleExports.hasOwnProperty('default');

hasCorrectExports = hasMessage && hasGreet && hasDefault;

__log("Module exports: " + Object.keys(moduleExports).join(", "));
__log("Has message: " + hasMessage);
__log("Has greet: " + hasGreet);
__log("Has default: " + hasDefault);

} catch (e) {
// Should get a regular module not found error, not ES module specific error
sourceMapRejected = e.message.indexOf("non-existent.mjs.map") !== -1;
__log("Error testing ES module exports: " + e.message);
}

expect(sourceMapRejected).toBe(true);
expect(hasCorrectExports).toBe(true);
});

it("should handle ES module loading alongside CommonJS", function () {
__log("TEST: Testing ES module and CommonJS coexistence");
it("should handle ES module functions correctly", function () {
__log("TEST: Testing ES module function execution");

var functionWorked = false;
var result = "";

// Test that we can still load regular JS modules
var regularModuleLoaded = false;
try {
var simpleModule = require("./simplemodule");
regularModuleLoaded = (simpleModule !== undefined);
var moduleExports = require("./test-es-module.mjs");

if (moduleExports.greet && typeof moduleExports.greet === 'function') {
result = moduleExports.greet("World");
functionWorked = (result === "Hello, World!");
__log("Function result: " + result);
} else {
__log("greet function not found or not a function");
}

} catch (e) {
// If simplemodule doesn't exist, that's okay for this test
regularModuleLoaded = true;
__log("Error testing ES module function: " + e.message);
}

expect(regularModuleLoaded).toBe(true);
expect(functionWorked).toBe(true);
expect(result).toBe("Hello, World!");
});

it("should attempt to load .mjs files through module system", function () {
__log("TEST: Testing .mjs file processing");
it("should maintain CommonJS compatibility", function () {
__log("TEST: Testing CommonJS compatibility with ES modules");

var commonJSWorks = false;
var esModuleWorks = false;

var mjsProcessingAttempted = false;
try {
// This will attempt to process the .mjs file we created
require("./test-es-module.mjs");
// Test that regular CommonJS modules still work
var simpleModule = require("./simplemodule");
commonJSWorks = true;
__log("CommonJS module loaded");
} catch (e) {
// Check that the error indicates the file was found and processing was attempted
// (It will likely fail because full ES module support isn't implemented yet)
mjsProcessingAttempted = e.message.indexOf("test-es-module") !== -1 ||
e.message.indexOf("module") !== -1;
// simplemodule might not exist, that's ok for this test
commonJSWorks = true; // Assume it would work
__log("CommonJS test skipped (module not found): " + e.message);
}

expect(mjsProcessingAttempted).toBe(true);
});

it("should handle optional module detection", function () {
__log("TEST: Testing optional module detection patterns");

// Test patterns that should be detected as likely optional modules
var bareModuleName = "lodash"; // bare module name
var relativePath = "./local-module"; // relative path
var absolutePath = "/app/absolute-module"; // absolute path

// We can't directly test IsLikelyOptionalModule, but we can test
// that the module system handles different path types appropriately
var allPatternsHandled = true;

// These should all result in appropriate error handling
try {
require(bareModuleName);
// Test that ES modules work alongside CommonJS
var esModule = require("./test-es-module.mjs");
esModuleWorks = (esModule !== null && esModule !== undefined);
__log("ES module works alongside CommonJS");
} catch (e) {
// Should get appropriate error for bare module
__log("ES module failed alongside CommonJS: " + e.message);
}

expect(commonJSWorks).toBe(true);
expect(esModuleWorks).toBe(true);
});

it("should not treat .mjs.map files as ES modules", function () {
__log("TEST: Testing source map exclusion");

// This test verifies that .mjs.map files are not treated as ES modules
var sourceMapCorrectlyRejected = true;

try {
require(relativePath);
// This should fail with module not found, not with ES module parsing
require("./non-existent.mjs.map");
sourceMapCorrectlyRejected = false; // Should not reach here
} catch (e) {
// Should get appropriate error for relative path
// Should get a regular module not found error
var isModuleNotFoundError = e.message.indexOf("non-existent.mjs.map") !== -1 ||
e.message.indexOf("Module not found") !== -1 ||
e.message.indexOf("Cannot find module") !== -1;
sourceMapCorrectlyRejected = isModuleNotFoundError;
__log("Source map error (expected): " + e.message);
}

expect(allPatternsHandled).toBe(true);
expect(sourceMapCorrectlyRejected).toBe(true);
});
});
1 change: 1 addition & 0 deletions test-app/runtime/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ add_library(
src/main/cpp/MethodCache.cpp
src/main/cpp/ModuleBinding.cpp
src/main/cpp/ModuleInternal.cpp
src/main/cpp/ModuleInternalCallbacks.cpp
src/main/cpp/NativeScriptException.cpp
src/main/cpp/NumericCasts.cpp
src/main/cpp/ObjectManager.cpp
Expand Down
129 changes: 126 additions & 3 deletions test-app/runtime/src/main/cpp/ModuleInternal.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* Author: gatanasov
*/
#include "ModuleInternal.h"
#include "ModuleInternalCallbacks.h"
#include "File.h"
#include "JniLocalRef.h"
#include "ArgConverter.h"
Expand All @@ -30,8 +31,11 @@ using namespace v8;
using namespace std;
using namespace tns;

// Global module registry for ES modules: maps absolute file paths → compiled Module handles
std::unordered_map<std::string, v8::Global<v8::Module>> g_moduleRegistry;

// Helper function to check if a module name looks like an optional external module
bool IsLikelyOptionalModule(const std::string& moduleName) {
bool ModuleInternal::IsLikelyOptionalModule(const std::string& moduleName) {
// Check if it's a bare module name (no path separators) that could be an npm package
if (moduleName.find('/') == std::string::npos && moduleName.find('\\') == std::string::npos &&
moduleName[0] != '.' && moduleName[0] != '~' && moduleName[0] != '/') {
Expand All @@ -41,7 +45,7 @@ bool IsLikelyOptionalModule(const std::string& moduleName) {
}

// Helper function to check if a file path is an ES module (.mjs) but not a source map (.mjs.map)
bool IsESModule(const std::string& path) {
bool ModuleInternal::IsESModule(const std::string& path) {
return path.size() >= 4 && path.compare(path.size() - 4, 4, ".mjs") == 0 &&
!(path.size() >= 8 && path.compare(path.size() - 8, 8, ".mjs.map") == 0);
}
Expand Down Expand Up @@ -324,9 +328,23 @@ Local<Object> ModuleInternal::LoadModule(Isolate* isolate, const string& moduleP

TryCatch tc(isolate);

// Check if this is an ES module (.mjs)
if (Util::EndsWith(modulePath, ".mjs")) {
// For ES modules, load using the ES module system
Local<Value> moduleNamespace = LoadESModule(isolate, modulePath);

// Create a wrapper object that behaves like a CommonJS module
// but exports the ES module namespace
moduleObj->Set(context, ArgConverter::ConvertToV8String(isolate, "exports"), moduleNamespace);

tempModule.SaveToCache();
result = moduleObj;
return result;
}

Local<Function> moduleFunc;

if (Util::EndsWith(modulePath, ".js") || Util::EndsWith(modulePath, ".mjs")) {
if (Util::EndsWith(modulePath, ".js")) {
auto script = LoadScript(isolate, modulePath, fullRequiredModulePath);

moduleFunc = script->Run(context).ToLocalChecked().As<Function>();
Expand Down Expand Up @@ -469,6 +487,111 @@ Local<Object> ModuleInternal::LoadData(Isolate* isolate, const string& path) {
return json;
}

Local<Value> ModuleInternal::LoadESModule(Isolate* isolate, const std::string& path) {
auto context = isolate->GetCurrentContext();

// 1) Prepare URL & source
string url = "file://" + path;
string content = Runtime::GetRuntime(isolate)->ReadFileText(path);

Local<String> sourceText = ArgConverter::ConvertToV8String(isolate, content);
ScriptCompiler::CachedData* cacheData = nullptr; // TODO: Implement cache support for ES modules

Local<String> urlString;
if (!String::NewFromUtf8(isolate, url.c_str(), NewStringType::kNormal).ToLocal(&urlString)) {
throw NativeScriptException(string("Failed to create URL string for ES module ") + path);
}

ScriptOrigin origin(isolate, urlString, 0, 0, false, -1, Local<Value>(), false, false,
true // ← is_module
);
ScriptCompiler::Source source(sourceText, origin, cacheData);

// 2) Compile with its own TryCatch
Local<Module> module;
{
TryCatch tcCompile(isolate);
MaybeLocal<Module> maybeMod = ScriptCompiler::CompileModule(
isolate, &source,
cacheData ? ScriptCompiler::kConsumeCodeCache : ScriptCompiler::kNoCompileOptions);

if (!maybeMod.ToLocal(&module)) {
if (tcCompile.HasCaught()) {
throw NativeScriptException(tcCompile, "Cannot compile ES module " + path);
} else {
throw NativeScriptException(string("Cannot compile ES module ") + path);
}
}
}

// 3) Register for resolution callback
// Safe Global handle management: Clear any existing entry first
auto it = g_moduleRegistry.find(path);
if (it != g_moduleRegistry.end()) {
// Clear the existing Global handle before replacing it
it->second.Reset();
}

// Now safely set the new module handle
g_moduleRegistry[path].Reset(isolate, module);

// 4) Instantiate (link) with ResolveModuleCallback
{
TryCatch tcLink(isolate);
bool linked = module->InstantiateModule(context, &ResolveModuleCallback).FromMaybe(false);

if (!linked) {
if (tcLink.HasCaught()) {
throw NativeScriptException(tcLink, "Cannot instantiate module " + path);
} else {
throw NativeScriptException(string("Cannot instantiate module ") + path);
}
}
}

// 5) Evaluate with its own TryCatch
Local<Value> result;
{
TryCatch tcEval(isolate);
if (!module->Evaluate(context).ToLocal(&result)) {
if (tcEval.HasCaught()) {
throw NativeScriptException(tcEval, "Cannot evaluate module " + path);
} else {
throw NativeScriptException(string("Cannot evaluate module ") + path);
}
}

// Handle the case where evaluation returns a Promise (for top-level await)
if (result->IsPromise()) {
Local<Promise> promise = result.As<Promise>();

// Process microtasks to allow Promise resolution
int maxAttempts = 100;
int attempts = 0;

while (attempts < maxAttempts) {
isolate->PerformMicrotaskCheckpoint();
Promise::PromiseState state = promise->State();

if (state != Promise::kPending) {
if (state == Promise::kRejected) {
Local<Value> reason = promise->Result();
isolate->ThrowException(reason);
throw NativeScriptException(string("Module evaluation promise rejected: ") + path);
}
break;
}

attempts++;
usleep(100); // 0.1ms delay
}
}
}

// 6) Return the namespace
return module->GetModuleNamespace();
}

Local<String> ModuleInternal::WrapModuleContent(const string& path) {
TNSPERF();

Expand Down
Loading