Javet icon indicating copy to clipboard operation
Javet copied to clipboard

Feature Request: Support Standard ESM Module Resolution for Node Engine

Open Alan-Gomes opened this issue 11 months ago • 2 comments

Description

Currently, Javet’s Node engine enforces a custom module resolution mechanism for ESM modules, which prioritizes security. While I understand and appreciate the security benefits of this approach, it can be limiting for certain use cases where the standard Node.js module resolution would be preferable.

I am aware that JavetBuiltInModuleResolver exists, but it is limited to resolving Node.js built-in modules. For more complex scenarios involving user-defined modules or dynamic resolution patterns, this is insufficient.

Although it is possible to implement a custom IV8ModuleResolver, reimplementing Node.js native module resolution is complex and error-prone, given the intricacies of its behavior.

Example code:

var runtime = V8Host.getNodeInstance().createV8Runtime();
var resolver = new JavetBuiltInModuleResolver();
runtime.setV8ModuleResolver((runtime1, specifier, referrer) -> {
  // here I could implement the same algorithm as Node.js, but the algorithm is complex and can be changed
  return resolver.resolve(runtime, specifier, referrer);
});

var executor = runtime.getExecutor(...);
executor.setModule(true);
executor.executeVoid();

Proposed Solution

Introduce an option to enable the standard Node.js ESM module resolver for the Node engine. This could be implemented as an opt-in feature, maintaining the current secure defaults while providing greater flexibility for advanced users.

Use Cases 1. Projects that rely on dynamic or non-trivial module resolution behaviors supported natively by Node.js. 2. Developers who need seamless integration with existing Node.js ESM workflows without having to replicate module resolution logic. 3. Scenarios where strict security restrictions are unnecessary or already handled elsewhere in the environment.

Thank you for considering this enhancement! Let me know if I can provide more details.

Alan-Gomes avatar Jan 01 '25 02:01 Alan-Gomes

Hi Alan,

Thank you for raising this issue. You are not alone.

There have been many Javet users asking for this for a couple of years. It's not easy to implement that without hacking the Node.js source code.

But, in general, I think it's technically achievable. My plan is as follows.

  1. Create public class NodeModule extends V8Module.
  2. Add public NodeModule createNodeModule(...) to NodeRuntime.

Thus, NodeRuntime will support both V8Module (supports module resolver) and NodeModule (doesn't support module resolver).

Please be aware that this will be a big feature that requires certain amount of time. You are welcome discussing that with me in real-time at discord, if you have any questions / suggestions.

caoccao avatar Jan 01 '25 07:01 caoccao

I don't know if the code below will help your case but here it is...

/*
 * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
 * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
 */
package com.khanhdz.javet;

import com.caoccao.javet.exceptions.JavetException;
import com.caoccao.javet.interop.V8Runtime;
import com.caoccao.javet.interop.callback.IV8ModuleResolver;
import com.caoccao.javet.interop.callback.JavetBuiltInModuleResolver;
import com.caoccao.javet.swc4j.Swc4j;
import com.caoccao.javet.swc4j.enums.Swc4jMediaType;
import com.caoccao.javet.swc4j.options.Swc4jTranspileOptions;
import com.caoccao.javet.values.reference.IV8Module;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

/**
 *
 * @author KhanhDzai - https://www.facebook.com/khanhdepzai.pro/
 */
public class CustomModuleResolver implements IV8ModuleResolver {

    private final Path rootPath;
    private final Swc4j swc4j = new Swc4j();

    private final JavetBuiltInModuleResolver resolverBuildIn = new JavetBuiltInModuleResolver();

    // Truyền vào thư mục gốc (nơi chứa các JS file)
    public CustomModuleResolver(Path rootPath) {
        this.rootPath = rootPath.toAbsolutePath().normalize();
    }

    @Override
    public IV8Module resolve(V8Runtime v8Runtime, String resourceName, IV8Module v8ModuleReferrer) throws JavetException {
        System.out.println("load: " + resourceName);
        if (resourceName.startsWith("node:")) {
            return resolverBuildIn.resolve(v8Runtime, resourceName, v8ModuleReferrer);
        }
        // Resolve path tương đối từ referrer
        Path referrerPath = Paths.get(v8ModuleReferrer.getResourceName()).getParent();
        Path resolvedPath = referrerPath.resolve(resourceName);

        // Nếu vẫn là relative, chuyển thành tuyệt đối dựa vào root
        if (resolvedPath.isAbsolute() == false) {
            resolvedPath = rootPath.resolve(resolvedPath);
        }

        // Chuẩn hóa path
        resolvedPath = resolvedPath.normalize();

        var file = resolvedPath.toFile();

        if (file.exists() == false) {
            throw new RuntimeException("Module not found: " + resolvedPath.toString());
        }

        try {
            resourceName = transpileIfNeeded(resolvedPath);
        } catch (Exception e) {
            throw new RuntimeException("ERROR compile code: " + resolvedPath.toString(), e);
        }

        return v8Runtime.getExecutor(resourceName)
                .setModule(true)
                .setResourceName(resolvedPath.toString())
                .compileV8Module();
    }

    private String getFileExtension(Path path) {
        String fileName = path.getFileName().toString();
        int lastDotIndex = fileName.lastIndexOf('.');
        if (lastDotIndex == -1 || lastDotIndex == fileName.length() - 1) {
            return ""; // Không có phần mở rộng
        }
        return fileName.substring(lastDotIndex + 1).toLowerCase(); // luôn viết thường
    }

    private String transpileIfNeeded(Path filePath) throws Exception {
        String code = Files.readString(filePath);
        String extension = getFileExtension(filePath);

        // Xác định media type
        Swc4jMediaType mediaType = switch (extension) {
            case "ts" ->
                Swc4jMediaType.TypeScript;
            case "tsx" ->
                Swc4jMediaType.Tsx;
            case "jsx" ->
                Swc4jMediaType.Jsx;
            default ->
                null; // Với .js hoặc .mjs thì dùng nguyên code
        };

        if (mediaType != null) {
            var output = this.swc4j.transpile(
                    code,
                    new Swc4jTranspileOptions()
                            .setSpecifier(filePath.toUri().toURL())
                            .setMediaType(mediaType)
            );
            code = output.getCode();
        }

        return code;
    }
}

SQKhanh avatar Apr 10 '25 22:04 SQKhanh