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

IMPORTANT: DO NOT USE! Use import mapping instead

Open Nytelife26 opened this issue 3 years ago • 54 comments

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.

Nytelife26 avatar Feb 21 '21 01:02 Nytelife26

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

Kehrlann avatar Feb 21 '21 09:02 Kehrlann

Yes, that is fine. I would be more than happy to open a pull request regarding versioning and use of import mapping where possible. Thank you for the response.

Nytelife26 avatar Feb 21 '21 16:02 Nytelife26

Further reading here and with a great example of directory mapping here. Hopefully this latter example clears up any confusion about the difference between "imports" and "exports" usage too

nick-bull avatar Feb 22 '21 11:02 nick-bull

@nick-bull Thanks for the extra examples :) greatly appreciated

Nytelife26 avatar Feb 22 '21 14:02 Nytelife26

An interest afterthought to replacing this module is that import mapping does not resolve directory imports, e.g. "#services/*": "./src/services/*.js" would not resolve import ... from '#services/someCoolService' to ./src/services/someCoolService/index.js.

You can get around this with "#services/*": "./src/services/*/index.js" but then that obviously doesn't work with non-index.js mappings.

The value is not really a regex, so something like "./src/services/*(/index)?.js" would not work. Directory-style imports with:

"#services/*": "./src/services/*.js"
import ... from '#services/someCoolService'

throws Error [ERR_UNSUPPORTED_DIR_IMPORT]: Directory import ... is not supported resolving ES modules imported from ...

Anybody know a way around this?

nick-bull avatar Feb 22 '21 16:02 nick-bull

@nick-bull ..uh

"#services/*": "./src/services/*.js"

Of course it doesn't? That'd resolve to ./src/services/someCoolService.js Use this instead:

"#services/*": "./src/services/*"

Nytelife26 avatar Feb 22 '21 19:02 Nytelife26

@nick-bull ..uh

"#services/*": "./src/services/*.js"

Of course it doesn't? That'd resolve to ./src/services/someCoolService.js Use this instead:

"#services/*": "./src/services/*"

I have already suggested that in my comment; I've edited my response so it's a little clearer, as it was a bit densely packed, as it does not work, it throws an error

nick-bull avatar Feb 22 '21 22:02 nick-bull

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

Nytelife26 avatar Feb 23 '21 08:02 Nytelife26

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

nick-bull avatar Feb 23 '21 14:02 nick-bull

No worries @nick-bull. My answer if not for your discovery of needing to set the type to modules would've been to just import #coolService/index.js if it wouldn't work the conventional way anyways, but I'm glad you found your problem.

If you have any further questions or concerns let me know.

Nytelife26 avatar Feb 24 '21 03:02 Nytelife26

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";

thebrownfox avatar Feb 26 '21 13:02 thebrownfox

@thefoxie thank you for the contribution :) however, this is not a place to report the package failing. I am glad the built-in functionality works, however, if you have any problems you should still create a separate issue so the ilearnio team can ensure the package works as intended for those who need it.

Nytelife26 avatar Feb 27 '21 00:02 Nytelife26

After reading the documentation, I still don't understand how to use it... Could you please explain to me how can I convert this:

"_moduleAliases": {
    "@root": ".",
    "@submodules": "submodules",
    "@db": "src/db",
    "@middleware": "src/middleware"
  }

to use native import mapping instead? Preferably without changing any other code. Thanks.

Papooch avatar Mar 08 '21 08:03 Papooch

@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 avatar Mar 09 '21 11:03 Nytelife26

Well that's what I tried also, but I am getting and error.

in C:\repos\project\src\db\connect.js, I am using

const { initModels } = require('#submodules/db-models/master');

But it throws an error, which I don't understand:

Error: Cannot find module 'C:\repos\project\submodules\db-models\master'
    at Object.<anonymous> (C:\repos\project\src\db\connect.js)

But the folder C:\repos\project\submodules\db-models\master DOES exist and has an index.js file in it which exports the initModels function.

Does folder mapping work differently to file mapping using this technique?

Papooch avatar Mar 09 '21 11:03 Papooch

@Papooch You didn't happen to get burned in the same way as I did? Test by using

const { initModels } = require('#submodules/db-models/master/index.js');

nick-bull avatar Mar 09 '21 12:03 nick-bull

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

Papooch avatar Mar 09 '21 12:03 Papooch

@Papooch I suspect you're being burned for the same reason as the prior mentioned comment, this feature isn't going to affect extension resolution. Try node --es-module-specifier-resolution=node, or install esm and use node -r esm. Report back and let us know if that works!

nick-bull avatar Mar 09 '21 14:03 nick-bull

I've gotten this to work as described above.

In my package.json

    "imports": {
        "#app/*": "./dist/app/*",
        "#lib/*": "./dist/lib/*",
        "#src/*": "./dist/*"
    },

then using node --es-module-specifier-resolution=node after compilation.

Where I am having issues, is when I try and use jest. Previously when using module-alias I could do the following in jest.config.ts

    moduleNameMapper: {
        '^#app/(.*)$': '<rootDir>/src/app/$1',
        '^#lib/(.*)$': '<rootDir>/src/lib/$1',
        '^#src/(.*)$': '<rootDir>/src/$1'
    },

but when I try and run tests, now, I am getting:

    Configuration error:

    Could not locate module #app/index.js mapped as:
    /Users/blah/src/app/$1.

    Please check your configuration for these entries:
    {
      "moduleNameMapper": {
        "/^#app\/(.*)$/": "/Users/blah/src/app/$1"
      },
      "resolver": undefined
    }

    > 1 | import { FastifyApp } from '#app/index.js';

It looks like it is looking for #app/index.js instead of #app/index.ts

If I do a quick hardcode hack, the test runs (I hardcode index.ts):

    moduleNameMapper: {
        '^#app/(.*)$': '<rootDir>/src/app/index.ts',
        '^#lib/(.*)$': '<rootDir>/src/lib/$1',
        '^#src/(.*)$': '<rootDir>/src/$1'
    },

...any ideas?

initplatform avatar Mar 12 '21 21:03 initplatform

@Papooch if you wish to use extensions in the map to avoid having to specify .js you can. Although, as people have mentioned, it is better to set the module resolution to use node's.

@initplatform how are the imports specified in the actual files?

Nytelife26 avatar Mar 12 '21 22:03 Nytelife26

Ha! I was banging my head against the monitor for a good hour earlier before I wrote that post. 5 minutes after trying to respond to your question I found it. Thanks!

For whatever reason, when I was debugging the transition over to import mapping I had done this:

import { FastifyApp } from '#app/index.js';

instead of this:

import { FastifyApp } from '#app/index';

Can't even remember what I was testing... but I forgot to remove the .js after whatever I was doing. Building and running worked fine, but jest exposed the error.

Sometimes it's the tiny things right :)

I am excited to see this import mapping land in node. At first I was averse to the # instead of using the @, but I kinda like it now. I am assuming it's to differentiate between npm namespaces and actual mappings...

Thanks to all involved that created module-alias and for your response @Nytelife26

initplatform avatar Mar 13 '21 01:03 initplatform

@initplatform I'm not sure you'll need the index either, as I'm pretty sure directory imports are part of --es-module-specifier-resolution=node

nick-bull avatar Mar 13 '21 13:03 nick-bull

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!

Nytelife26 avatar Mar 13 '21 19:03 Nytelife26

@initplatform I'm not sure you'll need the index either, as I'm pretty sure directory imports are part of --es-module-specifier-resolution=node

I believe that's an annoyance with typescript.

For whatever reason, if only using the typescript path alias, I need to specify index or typescript won't compile. I don't need to use index for nested routes though.

Screen Shot 2021-03-15 at 9 52 45 AM Screen Shot 2021-03-15 at 9 55 44 AM

initplatform avatar Mar 15 '21 13:03 initplatform

One other reason to keep maintaining this package is that import mapping only works for "type": "module", which isn't ideal for e.g., React Native

nick-bull avatar Mar 29 '21 11:03 nick-bull

One other reason to keep maintaining this package is that import mapping only works for "type": "module", which isn't ideal for e.g., React Native

Is that so? Standard React and JSX relies on ESM, I cannot see why React Native wouldn't permit it.

Nytelife26 avatar Mar 30 '21 09:03 Nytelife26

I've discovered that I might need to switch to es6 modules, and that module-alias seems to be not working. I stumbled across this thread and converted my project over to using the "imports" property in package.json, but I'm getting an error I can't solve:

Package.json:

"imports": {
    "#Common/*": "../Common/*.js",
    "#Typescript/*":  "../Typescript/*.js"
  }

Error:

Invalid "imports" target "../Common/*.js" defined for '#Common/*' in the package config [PathToRepo]\Server\package.json imported from [PathToRepo]\Server\server.js

Does this mean that the inbuilt "imports" property doesn't work outside the directory that the package.json is located within? Is there a work around for this? If not is there a way to make module-alias work with ES6 modules?

Griffork avatar May 04 '21 13:05 Griffork

Does this mean that the inbuilt "imports" property doesn't work outside the directory that the package.json is located within?

Do I get to ask why you're trying to use import paths to import from directories not contained within your project? The paths you're aliasing should always be contained within the project that the package.json is for.

It appears that you're working on a project with multiple subpackages. If that is the case, use the import paths in the package.json for the top level directory of the project.

I hope this helps.

Nytelife26 avatar May 05 '21 17:05 Nytelife26

I can't figureout how to get node 16 native ESM support to work with Mocha 8 and module-alias... So this is my only option

Error on node 16, mocha 8, type: "module"

Error [ERR_MODULE_NOT_FOUND]: Cannot find package '@src/common' imported from /src/mocking/reset.js
    at packageResolve (internal/modules/esm/resolve.js:655:9)
    at moduleResolve (internal/modules/esm/resolve.js:696:18)
    at Loader.defaultResolve [as _resolve] (internal/modules/esm/resolve.js:810:11)
    at Loader.resolve (internal/modules/esm/loader.js:86:40)
    at Loader.getModuleJob (internal/modules/esm/loader.js:230:28)
    at ModuleWrap.<anonymous> (internal/modules/esm/module_job.js:56:40)
    at link (internal/modules/esm/module_job.js:55:36)

mocha test wrapper

import moduleAlias from 'module-alias'
moduleAlias()

import test from '../app.test.js'
export default test

package.json

"_moduleAliases": {
    "@src": "./src"
},

reset.js

import { simple as logger } from '@src/common/Logger.js'

FossPrime avatar May 07 '21 19:05 FossPrime

@Nytelife26 because I have the following directory structure:

  • Client
  • Common
  • Server

Client and server both include common, so it can't be "within" either project.

Also WHY do you need to enforce a very arbitrary condition of "you can not alias any code outside of the directory of your project'? Shared node_modules folders, shared code and external libraries being outside of your codebase (for the purposes of cleaner commits to git repositories and such) are not uncommon occurances in the wild.

My top level doesn't have a package.json (because it's not a package ...?). Having package.jsons that exist alone with no actual code associated with them seems like a bad design choice.

Instead I hacked an experimental node API to allow accessing paths outside of the current directory.

Griffork avatar May 23 '21 05:05 Griffork