Skip to content

Best effort Module Resolution Algorithm implementation based on Node.js 25.2.1 - #971

Merged
graalvmbot merged 2 commits into
oracle:masterfrom
imakira:feat/module-resolution
Feb 5, 2026
Merged

Best effort Module Resolution Algorithm implementation based on Node.js 25.2.1#971
graalvmbot merged 2 commits into
oracle:masterfrom
imakira:feat/module-resolution

Conversation

@imakira

@imakira imakira commented Jan 8, 2026

Copy link
Copy Markdown
Contributor

Best effort Module Resolution Algorithm implementation based on Node.js 25.2.1 as documented in https://nodejs.org/api/esm.html#resolution-and-loading-algorithm.

The following features are not implemented:

  • DETECT_MODULE_SYNTAX(source) is not implemented
  • In ESM_FILE_FORMAT, node.js will try to guess the format of a file with no extension. I kept the original behavoir of esmFileFormat which is throwing an error (Otherwise it will break some tests on some other places where I dont want to touch...)
  • ".wasm" files are not handled by ESM_FILE_FORMAT

This patch breaks one unit test:

@Test
public void dontImportCommonJs() throws IOException {
	final String src = "import('with-package').then(x => {throw 'unexpected'}).catch(console.log);";
	final String out = "TypeError: Unsupported file extension: '" + getTestRootFolderUrl() + "node_modules/with-package/alternative-index.js'\n";
	runAndExpectOutput(src, out);
}

The previous behavoir with regard to using import with CommonJS modules was:

  • import() can't import a commonjs module with .js extension, in a package not marked with "type"="module"
  • import() can import a commonjs module with .cjs extension.
  • import() can import a commonjs module with .js extension but not in a package

As import/import() can be used to load JavaScript CommonJS modules stated in packages.html.
I believe it is fair to expect import/import() to be able to import CommonJS modules.
In this patch, import can import a commonjs module with a cjs extension, or with a js extension and in a package not marked with "type"="module".

I couldn't get WebAssemblySimpleTestSuite working correctly (error: WebAssembly API enabled but "wasm" language cannot be accessed! Make sure the "wasm" l
anguage is found on the module path and one of the permitted languages when creating the Context). All other tests are running correctly.

Another thing to note:

In this patch, if esmResolve returns a result and esmFileFormat returns CommonJs, it will pass the resoluted path directly to tryLoadingAsCommonjsModule. Instead of the original specifier.
I don't know if this is the best way to do it, but at least we can import a CommonJs module exported in exports using import now (ideally we should make require also support it).

This patch is based on #904

@oracle-contributor-agreement

Copy link
Copy Markdown

Thank you for your pull request and welcome to our community! To contribute, please sign the Oracle Contributor Agreement (OCA).
The following contributors of this PR have not signed the OCA:

To sign the OCA, please create an Oracle account and sign the OCA in Oracle's Contributor Agreement Application.

When signing the OCA, please provide your GitHub username. After signing the OCA and getting an OCA approval from Oracle, this PR will be automatically updated.

If you are an Oracle employee, please make sure that you are a member of the main Oracle GitHub organization, and your membership in this organization is public.

@oracle-contributor-agreement oracle-contributor-agreement Bot added the OCA Required At least one contributor does not have an approved Oracle Contributor Agreement. label Jan 8, 2026
@imakira

imakira commented Jan 16, 2026

Copy link
Copy Markdown
Contributor Author

Hello, any update on this PR?

@woess

woess commented Jan 27, 2026

Copy link
Copy Markdown
Member

@imakira thank you for your contribution! We'll try to review this PR soon, but we'll need the OCA check to be cleared first.
You mentioned in #960 (comment) that you already signed the OCA but that it is under review? Because it still does not show up as signed.

@imakira

imakira commented Jan 27, 2026

Copy link
Copy Markdown
Contributor Author

@woess Thanks for you reply!
Last time my application got rejected for reason, I re-submitted one just a moment ago.

@oracle-contributor-agreement

Copy link
Copy Markdown

Thank you for signing the OCA.

@oracle-contributor-agreement oracle-contributor-agreement Bot added OCA Verified All contributors have signed the Oracle Contributor Agreement. and removed OCA Required At least one contributor does not have an approved Oracle Contributor Agreement. labels Jan 27, 2026
@woess
woess requested review from iamstolis and woess January 27, 2026 13:45
@iamstolis

Copy link
Copy Markdown
Member

I couldn't get WebAssemblySimpleTestSuite working correctly ...

WebAssemblySimpleTestSuite needs wasm. You can build graaljs with wasm using mx --dy /wasm build (in graal-js project directory) and then execute the test-suite using mx --dy /wasm unittest WebAssemblySimpleTestSuite

Speaking of useful mx commands, you may want to reformat the code using the formatter that we use (using mx eclipseformat --primary). Of course, we can reformat it ourselves before the integration of this PR, but doing so will steal some credit from you (in the sense of git blame).

private URI packageImportsResolve(String specifier, URI parentURL, List<String> conditions, TruffleLanguage.Env env) {
// 1. Assert: specifier begins with "#".
// 2. If specifier is exactly equal to "#" or starts with "#/", then
if(!specifier.startsWith("#") || specifier.equals("#") || specifier.equals("#/")){

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no need to check for !specifier.startsWith("#"). packageImportsResolve() is always called with a specifier that begins with # (as the first step of this method says).

It seems that you can also remove the check for #/ that is not part of the algorithm (anymore?).

} else if(exports instanceof JSDynamicObject exportsObj &&
exportsObj.hasOwnProperty(constant(DOT))){
// 2.3.1 Set mainExport to exports["."].
mainExport = JSObject.get(exportsObj, constant(DOT));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use Strings.DOT instead of constant(DOT) here (and two lines above).

}

private static int countChar(String s, char c){
return s.length() - s.replace(String.valueOf(c), "").length();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is always used to check whether there is exactly one occurrence of c in s. This can be implemented more efficiently. For example:

    private static boolean containsOnlyOne(String s, char c) {
        int first = s.indexOf(c);
        return (first != -1) && (first == s.lastIndexOf(c));
    }

// 2.1 If target contains any index property keys, as defined in ECMA-262 6.1.7 Array
// Index, throw an Invalid Package Configuration error.
for (var key : targetObj.ownPropertyKeys()) {
if (!(key instanceof TruffleString)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This condition should check for index property keys. These are keys that are strings but can be parsed as array indices. So, the current check is incorrect. JSRuntime.isArrayIndex(key) is more appropriate here.

// throw an Invalid Package Configuration error.
boolean hasStartingWithDot = false;
if(exports instanceof JSDynamicObject exportsObj){
List<TruffleString> keys = JSObject.enumerableOwnNames(exportsObj);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no need to check for enumerability. You can use ownPropertyKeys() instead of enumerableOwnNames()

@iamstolis

Copy link
Copy Markdown
Member

Thank you for your contribution. We will be happy to integrate it into graaljs. I pointed out few things that can be improved. Let me know if you want to do that yourself. We can always do these changes ourselves when integrating your PR.

sgammon and others added 2 commits February 3, 2026 12:58
If a module `package.json` specifies `exports`, GraalJs will now read
them and prefer exports over standard resolution; export types can be
registered by the developer as preferred. In lieu of these types (and
as a default), the following export types are preferred, in order:

- `graaljs`
- `import` (in ESM)
- `require`
- `default`

Fixes and closes oracle#903

Relates-to: oracle#903
Signed-off-by: Sam Gammon <sam@elide.dev>
@imakira
imakira force-pushed the feat/module-resolution branch from 64b283e to 849f318 Compare February 3, 2026 06:35
@imakira

imakira commented Feb 3, 2026

Copy link
Copy Markdown
Contributor Author

@iamstolis Thanks for pointing out the parts I have missed!

I have made the changes you suggested, formatted the code, fixed one unit test I somehow missed last time (unsupportedUrl test, about error message), and rebased the PR against the master branch.

Let me know if there's some thing I'm still missing.

@graalvmbot
graalvmbot merged commit 849f318 into oracle:master Feb 5, 2026
1 check passed
@iamstolis

Copy link
Copy Markdown
Member

@imakira, thank you again for your contribution. The PR has been merged into master branch. So, it will be part of daily builds and will appear in the next release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

OCA Verified All contributors have signed the Oracle Contributor Agreement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants