ESLint import resolver for ESM modules via package.json exports map.
Relies on NPM package resolve.exports
https://github.com/lukeed/resolve.exports
See:
module.exports = { | |
settings: { | |
'import/resolver': { | |
[path.resolve('./eslint-plugin-import-resolver.cjs')]: { someConfig: 1 }, | |
}, | |
}, | |
}, |
ESLint import resolver for ESM modules via package.json exports map.
Relies on NPM package resolve.exports
https://github.com/lukeed/resolve.exports
See:
const path = require('path'); | |
const { resolve: resolveExports } = require('resolve.exports'); | |
/** | |
* @param {string} source source | |
* @param {string} file file | |
* @param {Object} _config config | |
*/ | |
const resolve = (source, file, _config) => { | |
try { | |
const moduleId = require.resolve(source, { paths: [path.dirname(file)] }); | |
return { found: true, path: moduleId }; | |
} catch (/** @type {any} */ err) { | |
if (err.code === 'MODULE_NOT_FOUND' && err.path?.endsWith('/package.json')) { | |
const { name, module, main, exports } = require(err.path); | |
const resolved = resolveExports({ name, module, main, exports }, source); | |
const moduleId = path.join(path.dirname(err.path), resolved); | |
return { found: true, path: moduleId }; | |
} | |
return { found: false }; | |
} | |
}; | |
module.exports = { | |
interfaceVersion: 2, | |
resolve, | |
}; |
Thank you. I am actually using a slightly different technique in my project, which I probably figured out after I posted this Gist (note to self: update the Gist!)
I am not sure which technique is "best" (in terms of of how the resolver stack is expected to work). Here's my additions:
const { builtinModules } = require('module');
const builtins = new Set(builtinModules);
// ...
const resolve = (source, file, _config) => {
if (builtins.has(source)) {
return { found: true, path: null };
}
// ...
... as opposed to your fix which returns false
after a successful require.resolve()
:
const { builtinModules } = require('module');
// ...
const resolve = (source, file, _config) => {
// ...
const moduleId = require.resolve(source, { paths: [path.dirname(file)] });
if (builtinModules.includes(moduleId)) {
return { found: false };
}
// ...
What do you think? (I'll run some tests at my end)
Ok, so I ran some additional tests.
Both "my" and "your" builtinModules
solutions work (I tested them one by one, and I added an import typo like fss
instead of fs
to check false negatives / positives), ... however note in my case I have the following ESLint config:
settings: {
react: {
version: '17',
},
'import/parsers': {
'@typescript-eslint/parser': ['.ts', '.tsx'],
},
'import/resolver': {
// https://github.com/alexgorbatchev/eslint-import-resolver-typescript
typescript: {
alwaysTryTypes: false,
project: ['./PATH/TO/tsconfig.json'],
},
[path.resolve('./eslint-plugin-import-resolver.cjs')]: { someConfig: 1 },
},
}
...the important part is the import/resolver
> typescript
object, which when commented-out triggers either of our builtinModules
conditionals (for example, for import { readFileSync } from "fs"
). However if I leave my typescript
settings in, then the builtinModules
conditionals are never reached, as NodeJS module imports such as fs
seem to be captured appropriately somewhere else in the ESLint resolver chain, outside of my custom resolver.
Hello.
Thank you for your problem resolve!
I have opinion on catching error.
//...
catch (/** @type {any} */ err) {
if (err.code === 'MODULE_NOT_FOUND' && err.path?.endsWith('/package.json')) {
const { name, module, main, exports } = require(err.path);
const resolved = resolveExports({ name, module, main, exports }, source);
const moduleId = path.join(path.dirname(err.path), resolved);
return { found: true, path: moduleId };
}
//...
In my case, use subpath on node.
When cannot resolve moduleId
in try, err.path
is always package.json
.
When moduleId
cannot be defined in the try syntax, the error.path
in the catch syntax is always package.json
.
I think, printing a path that cannot be resolved is much useful. also it is already defined in 'source'.
EX:) error Unable to resolve path to module '#utils/error' import/no-unresolved
Then, what kind of error situation should the following syntax be executed?
//...
const { name, module, main, exports } = require(err.path);
const resolved = resolveExports({ name, module, main, exports }, source);
const moduleId = path.join(path.dirname(err.path), resolved);
//...
Thank you. :)
Thanks for that solution.
I had to change the following line:
const possibleBuildinModuleId = moduleId.startsWith('node:') ? moduleId.slice(5) : moduleId;
if (builtinModules.includes(possibleBuildinModuleId)) {
return { found: false };
}
Nevertheless, I get the following error:
build-scripts\create-run-env\index.js:25:8
✖ 25:8 Relative imports from parent directories are not allowed. Please either pass what you're importing through at runtime (dependency injection), move index.js to same directory as #build-scripts/constants.js or consider making #build-scripts/constants.js a package. import/no-relative-parent-imports
1 error
So, this solution does not cooperate with rule import/no-relative-parent-imports
Ran into an issue where this resolver would crash other eslint rules because it was trying to resolve builtin modules incorrectly.
Specifically this error, for
import/no-cycle
:Fixed it with the following snippet: