module-alias icon indicating copy to clipboard operation
module-alias copied to clipboard

Not working with native ES modules

Open cullylarson opened this issue 6 years ago • 27 comments

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.

cullylarson avatar May 21 '19 03:05 cullylarson

@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

jdt3969 avatar Jun 10 '19 16:06 jdt3969

@jdt3969 Thanks for sharing. I'll give it a try on my next node project.

cullylarson avatar Jun 10 '19 19:06 cullylarson

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 ?

Kehrlann avatar Jun 10 '19 19:06 Kehrlann

@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.

cullylarson avatar Jun 10 '19 23:06 cullylarson

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.

kirkouimet avatar Nov 27 '19 18:11 kirkouimet

Not yet solved natively without execution flags?

eouia avatar Sep 01 '20 08:09 eouia

@kirkouimet @eouia the node ems documentation linked above suggests no, the flag is required.

JakobJingleheimer avatar Jan 05 '21 22:01 JakobJingleheimer

@jdt3969's above solution worked for me after I realised it was looking for a property in package.json called aliases instead of module-alias's documented _moduleAliases (I prefer jdt3969's aliases).

I tidied up the above code and added some pre-computing of values that can't change during execution: gist

EDIT: ~⚠️ This appears to have broken somewhere between node v15.1.0 and v15.6.0 (jdt3969's too)~

This was due to an error in my own source-code, which node ESM erroneously reported as coming from alias-loader.mjs

JakobJingleheimer avatar Jan 05 '21 23:01 JakobJingleheimer

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?

TimDaub avatar Jan 15 '21 13:01 TimDaub

@TimDaub Thanks!

But I'm wondering if this functionality can already be used when publishing a module to npm.

You probably shouldn't as loaders are imminently changing.

how am I supposed to specify --experimental-loader=./alias-loader.mjs ./index.mjs as a user of said module?

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? For both, the answer would be roughly the same: as loaders currently stand in their experimental form, the consuming user would need to manually add it to their command (ex in package.json's "scripts") regardless of "who" is using it.

JakobJingleheimer avatar Jan 15 '21 20:01 JakobJingleheimer

@jdt3969's above solution worked for me after I realised it was looking for a property in package.json called aliases instead of module-alias's documented _moduleAliases (I prefer jdt3969's aliases).

Worked for me as well, though it's important to note that you now need to use --experimental-loader=./custom-loader.mjs. I added as well --experimental-json-modules so that it works with .json files.

jaschaio avatar Mar 31 '21 12:03 jaschaio

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.

TimDaub avatar Mar 31 '21 12:03 TimDaub

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.

szydlovski avatar Sep 21 '21 18:09 szydlovski

@szydlovski good point, would you want to submit a PR?

Kehrlann avatar Sep 22 '21 07:09 Kehrlann

@Kehrlann Sure, here's a proposal

szydlovski avatar Sep 22 '21 08:09 szydlovski

@TimDaub "unusable" does not seem accurate at all, and such CLI flags are quite common in many, many libraries. Support for a non-CLI option has been briefly discussed as a possibility in future, but if it happens, it would likely be quite a bit down the road. How loaders in Node.js will (almost surely) work once loader chaining is supported would be something like

$> node --loader https-loader --loader typescript-loader ./your-app.ts

JakobJingleheimer avatar Oct 23 '21 11:10 JakobJingleheimer

Having to mention in the installation instruction on the library level that the using application should be initiated with a loader flag makes it unusabe.

It's fragile declaring this information in a readme. On the library level, the loader dependency should be described in the package.json and any using higher level package should interpret the lower libraries' loaders through the package.json.

But I guess that's out of scope for module-alias.

TimDaub avatar Oct 23 '21 12:10 TimDaub

That sounds a bit dramatic. It's called a README (in screaming capitals) for a reason…. This is classic RTFM.

Module alias is obsolete now anyway with import mapping.

JakobJingleheimer avatar Oct 23 '21 17:10 JakobJingleheimer

This problem has been plaguing me for years because I want to write good quality code that is shared between node & browser. I finally found a system that works:

  • Place 'nesm.js' in the root of your project
  • [optional] Place the 'nesm' shell script in your path, make it executable
  • Run scripts with: 'nesm file_to_run.js' or 'node path/to/nesm.js -- file_to_run.js'

'nesm.js'

/*
 * esm and module-alias do not play nicely together.
 * this precise arrangement is the only way I found to make it work.
 * you can run this from anywhere in your project hierarchy.
 * you can use args, and use in npm scripts.
 * encourage the node.js devs to make this work natively.  ux matters.
 * ---- CAVEATS
 * will not work with "type":"module"
 * ---- SETUP
 * place 'nesm.js' in the root of your project
 * [optional] place the 'nesm' shell script in your path, make it executable
 * ---- USAGE
 * > nesm file_to_run.js
 * to run without the nesm shell script:
 * > node path/to/nesm.js -- file_to_run.js
 * to run with nodemon:
 * > nodemon -- path/to/nesm.js -- file_to_run.js
*/
require = require('esm')(module);   // eslint-disable-line no-global-assign
require('module-alias/register');   // must come after esm for some reason

let runNext;
for(const arg of process.argv) {
    if(runNext) {
        let filename = arg;
        if(filename[0]!='.' && filename[0]!='/') filename = './'+filename;
        require(filename);
        break;
    }
    runNext = (arg=='--');
}

'nesm' shell script

#!/bin/bash
if [ -z $1 ]; then
    echo "Node esm runner.  Usage: nesm file_to_run.js"
    exit 1
fi
baseDir=$( pwd )
while [ ! -f "$baseDir/nesm.js" ]; do
    if [ ${#baseDir} -le 1 ]; then
        echo "nesm.js not found in folder ancestry"
        exit 1
    fi
    baseDir="$(dirname "$baseDir")"
done
file1=$(realpath $1);
node $baseDir/nesm.js -- $file1

TheDirigible avatar May 10 '22 01:05 TheDirigible

@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...

euberdeveloper avatar Jul 18 '22 22:07 euberdeveloper

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

@euberdeveloper I did https://github.com/ilearnio/module-alias/issues/59#issuecomment-754960378 you can also see it on line 9 of the code. You're welcome btw.

JakobJingleheimer avatar Jul 19 '22 10:07 JakobJingleheimer

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

euberdeveloper avatar Nov 13 '22 18:11 euberdeveloper

Heads up: the node loader API function signatures have changed, and the 2nd argument is an (optional) object (not parentURL). Supplying an argument of invalid type will trigger an exception. Please see the Node.js docs for current signatures.

JakobJingleheimer avatar Nov 13 '22 19:11 JakobJingleheimer

@JakobJingleheimer

Could you please link me the signature in the doc? Does this mean that my noduek should behave differently depending on the version of nodejs?

euberdeveloper avatar Nov 13 '22 20:11 euberdeveloper

https://nodejs.org/api/esm.html#hooks

I'm not sure what version(s) of Node.js the change was backported to, but at the very least, my code from ~2 years ago and what you included in your npm package will not work in the v18 (LTS) or v19 (current) of node.

JakobJingleheimer avatar Nov 13 '22 20:11 JakobJingleheimer

https://nodejs.org/api/esm.html#hooks

I'm not sure what version(s) of Node.js the change was backported to, but at the very least, my code from ~2 years ago and what you included in your npm package will not work in the v18 (LTS) or v19 (current) of node.

My repo does some tests with also versions of nodejs 18 and 19, and it seems to work

euberdeveloper avatar Nov 14 '22 01:11 euberdeveloper

For anyone else coming across this looking to support wildcard imports replace:

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

With

const isAliasInSpecifier = (path, alias) => {
  return path.indexOf(alias) === 0
}

foges avatar Oct 31 '23 07:10 foges