module-alias: Not working with native ES modules

I’m moving a project over to using node’s native ES modules (enabled with the --experimental-modules flag). After updating my code, module-alias is no longer working. I tried adding this to the root of my app (this is the method I was using before transitioning to esm):

require('module-alias/register')

I tried changing it to:

import 'module-alias/register'

I tried requiring when starting the server:

node --experimental-modules -r module-alias/register server/app.js

The first aliased import in my app is this:

import {responseError} from '@app/lib/response'

I’m getting this error from it:

internal/modules/esm/default_resolve.js:69
  let url = moduleWrapResolve(specifier, parentURL);
            ^

Error: Cannot find package '@app/lib' imported from server/app.js
    at Loader.resolve [as _resolve] (internal/modules/esm/default_resolve.js:69:13)
    at Loader.resolve (internal/modules/esm/loader.js:70:33)
    at Loader.getModuleJob (internal/modules/esm/loader.js:143:40)
    at ModuleWrap.<anonymous> (internal/modules/esm/module_job.js:43:40)
    at link (internal/modules/esm/module_job.js:42:36)

The relevant lines in my package.json are:

"_moduleAliases": {
    "@app": "./server"
},

I’m starting the app like this:

node --experimental-modules server/app.js

module-alias worked fine using CommonJS. The only change I made to the code was changing requires to imports.

About this issue

  • Original URL
  • State: open
  • Created 5 years ago
  • Reactions: 32
  • Comments: 27

Most upvoted comments

@cullylarson Ran into the same issue.

It appears that the new esm code isn’t running the _resolveFilename which is the core of this library. Based on the docs it looks as though they are moving off of this library’s hack and onto a standard feature: https://github.com/nodejs/node/blob/master/doc/api/esm.md#experimental-loader-hooks It’s still experimental though.

I rewrote and reduced a lot based on known things within my library but this code is working for me:

// custom-loader.mjs
import path from 'path';
import npmPackage from './package.json';

const getAliases = () => {

  const base = process.cwd();

  const aliases = npmPackage.aliases || {};

  const absoluteAliases = Object.keys(aliases).reduce((acc, key) =>
    aliases[key][0] === '/'
      ? acc
      : { ...acc, [key]: path.join(base, aliases[key]) },
    aliases)

  return absoluteAliases;

}

const isAliasInSpecifier = (path, alias) => {
  return path.indexOf(alias) === 0
    && (path.length === alias.length || path[alias.length] === '/')
}

const aliases = getAliases();

export const resolve = (specifier, parentModuleURL, defaultResolve) => {
  
  const alias = Object.keys(aliases).find((key) => isAliasInSpecifier(specifier, key));

  const newSpecifier = alias === undefined
    ? specifier
    : path.join(aliases[alias], specifier.substr(alias.length));

  return defaultResolve(newSpecifier, parentModuleURL);
}

Then: node --no-warnings --experimental-modules --es-module-specifier-resolution=node --loader ./custom-loader.mjs index.js

Not yet solved natively without execution flags?

I understand that this is kind of an issue with Node itself, but it would be nice if the README included a warning about this package not being compatible with native ES modules. I just spent an hour questioning my sanity only to find out that the feature I’ve been hopelessly debugging was never intended to work at all.

@Kehrlann Thanks for the kind invitation to do a PR. I really appreciate the way you presented it. I’m near a deadline on a project right now and about to start another, otherwise I would take you up on the offer.

In this code there is a problem with Windows paths.

@cullylarson Ran into the same issue. It appears that the new esm code isn’t running the _resolveFilename which is the core of this library. Based on the docs it looks as though they are moving off of this library’s hack and onto a standard feature: https://github.com/nodejs/node/blob/master/doc/api/esm.md#experimental-loader-hooks It’s still experimental though. I rewrote and reduced a lot based on known things within my library but this code is working for me:

// custom-loader.mjs
import path from 'path';
import npmPackage from './package.json';

const getAliases = () => {

  const base = process.cwd();

  const aliases = npmPackage.aliases || {};

  const absoluteAliases = Object.keys(aliases).reduce((acc, key) =>
    aliases[key][0] === '/'
      ? acc
      : { ...acc, [key]: path.join(base, aliases[key]) },
    aliases)

  return absoluteAliases;

}

const isAliasInSpecifier = (path, alias) => {
  return path.indexOf(alias) === 0
    && (path.length === alias.length || path[alias.length] === '/')
}

const aliases = getAliases();

export const resolve = (specifier, parentModuleURL, defaultResolve) => {
  
  const alias = Object.keys(aliases).find((key) => isAliasInSpecifier(specifier, key));

  const newSpecifier = alias === undefined
    ? specifier
    : path.join(aliases[alias], specifier.substr(alias.length));

  return defaultResolve(newSpecifier, parentModuleURL);
}

Then: node --no-warnings --experimental-modules --es-module-specifier-resolution=node --loader ./custom-loader.mjs index.js

You should have said that in package.json you expected the key aliases and no more _moduleAliases

I made an npm module using this code that fixes also that problem, check it out here: esm-module-alias

Very interesting ! So if I understand correctly, you must just provide a .js file in a --loader flag ; and that .js file be a module that exports a resolve function, which basically does what module-alias does. (There might be a chicken and egg problem if you want to use module-alias programmatically though)

So it’d be very easy to do a pull request (wink wink, nudge nudge) that exports a neat little module that wraps module-aliases’ resolve function, which can then be used like (rough idea, semantics TBD) :

node --no-warnings --experimental-modules --es-module-specifier-resolution=node --loader ./node_modules/module-alias/es6-loader.js index.js

@cullylarson @jdt3969 interested in doing a PR ?

@ilearnio any additional thoughts ?

I’m not sure I understand what you mean—are you talking about where your module would itself depends on the loader, or someone consuming your module would need to consume it via --experimental-loader?

Later. Imagine I published a package that relied on starting node with --experimental-loader but then I’d also publish it to npm for others to use it. Now they’d have to be aware that now their project too as to be started with --experimental-loader.

the consuming user would need to manually add it to their command (ex in package.json’s "scripts") regardless of “who” is using it.

That makes it unusable for npm packages until a version of node is out that removes the flag.

This is really interesting and the gist looks promising @jshado1.

But I’m wondering if this functionality can already be used when publishing a module to npm. I guess not, because how am I supposed to specify --experimental-loader=./alias-loader.mjs ./index.mjs as a user of said module?

You guys think we can do this without using the --loader flag and instead do it programmatically? Now that Module._resolveFilename isn’t being used for import statements I wonder what is. May dive into it this week.