Search code examples
javascriptnode.jsnode-moduleses6-modules

ESM equivalent for createRequire(path).resolve(module)


I am working on a JavaScript instrumentation engine where I traverse a source file's AST and recursively enqueue imported modules for instrumentation as well. For that, I need to resolve a module name to its source file.

For CommonJS modules, this is really straightforward:

/**
 * Resolves the given module to a path.
 * @param currentFilePath The file that is currently instrumented. Serves as base path for the resolver.
 * @param module The module to resolve to its file path.
 */
function resolveCommonJsModule(currentFilePath, module)
{
  let requireFunc = createRequire(currentFilePath);
  return requireFunc.resolve(module);
}

However, this does not work for ES modules loaded through import. If a library defines separate exports for ES and CommonJS modules in its package.json, this function will always return the CommonJS version. For example:

...
"exports": {
    ".": {
      "types": "./index.d.ts",
      "import": "./esm/index.js",  <-- ES module
      "default": "./index.js"      <-- CommonJS module, which is returned by function above
    },
},
...

I know that there is import.meta.resolve(), but this does not allow me to set a "base path" from which resolution is done. Instead, it is relative to the instrumentation script, which resides in an entirely different directory.

What is the ES module equivalent for resolving module names given a base path?


Solution

  • There actually is an unstable overload import.meta.resolve(specifier, parent), which would allow to resolve imports relative to a given parent module, similar to require.resolve.

    However, it needs the --experimental-import-meta-resolve flag, and does not appear to work with all modules: Resolving ./file.mjs is successful, but resolving some-package leads to an ERR_MODULE_NOT_FOUND.

    Fortunately, I was able to find a ponyfill that works around the problem, import-meta-resolve:

    import * as importMetaResolve from "import-meta-resolve";
    import { pathToFileURL } from "node:url";
    
    let modulePath = importMetaResolve.resolve(module, pathToFileURL(currentFilePath));
    

    This command yields correct paths for all CommonJS and ES modules.