module-alias: IMPORTANT: DO NOT USE! Use import mapping instead

A similar feature to this library is already available in node as a standard: import mapping.

Not only does it allow for directory mapping, but it also allows dependency aliasing, and works for both require and import (ESM) WITHOUT breaking resolution behaviour in production or other libraries like using this library does.

You have been warned.

I know this sounds hateful, however, that is not my intention. My intention is instead to spread awareness about this mostly undocumented feature (it is not shown in any package manifest documentation besides node’s, and it is relatively tucked away) so people can write better software without needing to use hacky libraries like this one and without adding unnecessary dependencies.

About this issue

  • Original URL
  • State: open
  • Created 3 years ago
  • Reactions: 60
  • Comments: 54 (2 by maintainers)

Most upvoted comments

@Papooch you will have to convert to using #alias instead of @alias, but that’s it.

"imports": {
    "#root/*": "./*",
    "#submodules/*": "./submodules/*",
    "#db/*": "./src/db/*",
    "#middleware/*": "./src/middleware/*"
}

I hope that helps.

@Nytelife26 , thanks for raising awareness. This was introduced “recently”, so on older versions of node, you might still want module-alias - therefore we probably won’t just “deprecate” the package.

Would you mind opening a PR to update the README of this package, explaining that import mapping exists and should be used instead?

To see a working example of using subpath imports with index.js files in a commonjs environment - see the forwardemail.net repo

package.json

  "imports": {
    "#config/*": "./config/*.js",
    "#config": "./config/index.js",
    "#helpers/*": "./helpers/*.js",
    "#models/*": "./app/models/*.js",
    "#models": "./app/models/index.js",
    "#controllers/*": "./app/controllers/*.js",
    "#controllers": "./app/controllers/index.js",
    "#controllers/web": "./app/controllers/web/index.js",
    "#controllers/api": "./app/controllers/api/index.js"
  },

Many people are confused because using these import maps uses the same resolution logic as ESModules, which do not automatically resolve index.js files in the same way commonjs does. ESM requires the full path and extension to the file it is importing, and by extension so do the import maps here.

Using import maps like shown above will help you have a similar feel to commonjs. If you go deeper in the linked repo - you can see that this works.

Also note that the * in the imports does not have the same functionality as a glob star, which can be a bit confusing and may need some trial and error to set up your project correctly.

This package didn’t work for me no matter how I hard I tried to configure it. Maybe because I use ES6+ including imports (no single require() in my project) Builtin functionality works.

"imports": {
    "##/*": "./*"
},

jsconfig

{
    "compilerOptions": {
        "baseUrl": ".",
        "paths": {
            "##/*": ["./*"]
        }
    },
    "exclude": ["node_modules"]
}

usage

import { Model } from "##/api/models.js";

@nick-bull Well, that does seem to work, but I have to explicitly state the file, including the file extension. require('#submodules/db-models/master/index') does not work. I would have to change imports in the whole project and lose the flexibility along the way.

I would very much prefer to use a native solution instead of a library, but not in a way that makes my experience worse.

For those who say changing ‘@’ to ‘#’ will break their codebase, is it possible for you to do a find/replace in your codebase to fix the imports? We’ve done something like that before when needing to do other sweeping changes across the codebase, and it’s worked out quite well as long as there was a pattern we could match on initially.

the problem is not how to replace ‘@’ by ‘#’, the problem is in the project structure itself.

for example if you want to import a local package that is not published to npm and updated frequently without packing it everytime you can add it’s name to tsconfig path so @scope/package-name refers the location of the local package and allows you to import it in your project like this

import something from `@scope/package-name

which will be replaced with the actual path.

changing ‘@’ to ‘#’ will not help

@Nytelife26 Trying to switch to a node subpath imports, but for me it only works with the full path.

My imports:

  "imports": {
    "#utils/*": "./app/utils/*"
  }

Utils folder has two files: index.js and test.js, both has foo and bar functions.

My entry:

const {foo} = require('#utils');
const {bar} = require('#utils/test');
foo();
bar();

Both calls only work if I add the full path: #utils/index.js or #utils/test.js. Otherwise i have:

Package import specifier "#utils" is not defined in package C:\...\package.json imported from C:\...\test.js
and
Cannot find module 'C:\...\app\utils\test'

I have no "type": "module" enabled, and I tried "type": "commonjs" with no success. Also experimental-specifier-resolution does not help.

Any idea what I’m doing wrong?

@nick-bull No, you did not already suggest what I said specifically. You suggested something similar, which was "#services/*": "./src/services/*.js" as aforementioned. Although, actually, that still works - you’ll notice if you import #services/someCoolService/test, that does in fact resolve to ./services/someCoolService/test.js, for both require and import. And then if you do the other thing I suggested, "#services/*": "./src/services/*", you have to import #services/someCoolService/test.js to achieve the same result, but it will definitely work.

I hope that helps.

You’re right, I totally thought I’d written that example too and forgot to include it. I’m still not sure that answers my question though - is there a way to write a mapping that will allow both of the following:

import ... from '#coolService' // coolService/index.js
import ... from '#coolService/someFile.js' // coolService/someFile.js

Edit, scrap the above. It was because I had "type": "module" in package.json in my test project, causing it to throw the Error [ERR_UNSUPPORTED_DIR_IMPORT]: Directory import ... is not supported resolving ES modules imported from .. I’d mentioned before. You’ll either need transpilation or node --experimental-specifier-resolution=node .... Thanks Nytelife!

@jlenon7 Yeah I don’t think the path intellisense extensions support subpath imports, however, TS (and vscode ts linting) does. So while it won’t help you autocomplete, it will help validate their existence and types.

@Griffork That seems to be a relatively critical misunderstanding of how monorepositories work. They’re packages within a package structure.

Common is not part of either package. So, instead, you should make the other packages depend on common, assuming common is its own package.

As to why that needs to be a “very arbitrary condition”, how do you expect a package to work if it imports code that is neither bundled with the package nor depended on in the manifest?? If anyone installs that, it will break, guaranteed. Shared code and external libraries are common - that’s why you can make them dependencies.

You should look into workspaces for your relevant node package manager and research further into monorepositories.

No worries @initplatform. I give special credit to @nick-bull for the module resolution override because prior to that I was unaware it even existed honestly. I’m glad you found your solution, though!

Life on easy mode for typescript users: use tsx as a runtime instead of node. Everything just works, no need for additional config besides the aliases defined in tsconfig.json, no need to emit any js file either.

@spence-s when you use the Node.js ESM import aliases in your code to import modules, does VSCode understand the type of the class or function that you are importing? In my case I can only get the type of the class or function if I use the real path to import modules:

// Does not work. VSCode does not show intellisense for Hello class.
import { Hello } from '#src/hello'

// Works
import { Hello } from './src/Hello.js'

this will force you make some changes to your codebase that introduce breaking change

  1. you will need to change your alias to start with ‘#’
  2. es6 imports can’t refer to a path outside your module i.e a parent director for monorepos

Another solution is tsc-alias which generates relative paths. You can see my configuration for it.

A similar feature to this library is already available in node as a standard: import mapping.

Not only does it allow for directory mapping, but it also allows dependency aliasing, and works for both require and import (ESM) WITHOUT breaking resolution behaviour in production or other libraries like using this library does.

You have been warned.

I know this sounds hateful, however, that is not my intention. My intention is instead to spread awareness about this mostly undocumented feature (it is not shown in any package manifest documentation besides node’s, and it is relatively tucked away) so people can write better software without needing to use hacky libraries like this one and without adding unnecessary dependencies.

However, this works fine for me. VS Code Intellisense will not resolve the imports. Is there any convenient workaround for that?