jest
jest copied to clipboard
[Feature]: Support package imports in jest-resolve
🚀 Feature Proposal
Node Imports in packages.json
Motivation
https://nodejs.org/dist/latest-v16.x/docs/api/packages.html#imports
Example
No response
Pitch
similar as #9771
Is there a current work around for the imports feature in jest?
No, but once we have exports I guess imports shouldn't be too hard to build on top of it
I think this is not only a feature, but a bug.
Projects using some popular packages, such as chalk cannot use jest because of this bug...
An example is #12309.
If anyone is looking for a workaround for now: moduleNameMapper
"moduleNameMapper": {
"chalk": "chalk/source/index.js",
"#ansi-styles": "chalk/source/vendor/ansi-styles/index.js",
"#supports-color": "chalk/source/vendor/supports-color/index.js"
},
https://github.com/qiwi/libdefkit/blob/master/jest.config.json#L22
Once a module implementing the algorithm exists (e.g. https://github.com/lukeed/resolve.exports/issues/14) we should be good to go for adding support here.
This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 30 days.
I've been wrestling with this problem today. I thought I could address it by adding a resolver that uses enhanced-resolve, but apparently adding a resolver (which can only be commonjs?) breaks ESM processing, and suddenly every ESM dependency I use is unparsable.
Plugging in a resolver should work fine, but it needs to support all cases. You could choose to e.g. look for # as first char and delegate (default resolver is injected as argument), but regardless I'd assume it works fine?
~~This solution works for me:~~
moduleNameMapper: {
chalk: require.resolve("chalk/source/index.js"),
"#ansi-styles": require.resolve("ansi-styles/index.js"),
"#supports-color": require.resolve("supports-color/index.js"),
},
This solution works for me:
moduleNameMapper: {
chalk: require.resolve("chalk"),
"#ansi-styles": path.join(
require.resolve("chalk").split("chalk")[0],
"chalk/source/vendor/ansi-styles/index.js",
),
"#supports-color": path.join(
require.resolve("chalk").split("chalk")[0],
"chalk/source/vendor/supports-color/index.js",
),
},
https://github.com/facebook/jest/issues/12270#issuecomment-1111533936 does not work in applications where chalk v4 (without imports) and chalk v5 (with imports) coexist.
So I use the workaround of using jest's default resolver whenever possible. This also works for applications with both chalk v4 and chalk v5.
// jest.config.mjs
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
const dir = join(dirname(fileURLToPath(import.meta.url)));
const jestConfig = {
// ...
resolver: join(dir, 'src/test-util/jest/resolver.cjs'),
};
export default jestConfig;
// src/test-util/jest/resolver.cjs
// @ts-check
const { dirname, resolve } = require('path');
const resolveFrom = require('resolve-from');
/**
* @typedef {{
* basedir: string;
* conditions?: Array<string>;
* defaultResolver: (path: string, options: ResolverOptions) => string;
* extensions?: Array<string>;
* moduleDirectory?: Array<string>;
* paths?: Array<string>;
* packageFilter?: (pkg: any, file: string, dir: string) => any;
* pathFilter?: (pkg: any, path: string, relativePath: string) => string;
* rootDir?: string;
* }} ResolverOptions
* */
/** @type {(path: string, options: ResolverOptions) => string} */
module.exports = (path, options) => {
// workaround for https://github.com/facebook/jest/issues/12270
if (path === '#ansi-styles' || path === '#supports-color') {
const chalkRoot = resolve(dirname(resolveFrom(options.basedir, 'chalk')), '../');
const subPkgName = path.slice(1);
return `${chalkRoot}/source/vendor/${subPkgName}/index.js`;
}
return options.defaultResolver(path, options);
};
from: https://github.com/mizdra/eslint-interactive/blob/c6383db807ec31b6329b744ce5d2b2897adf085e/src/test-util/jest/resolver.cjs
import resolve from 'enhanced-resolve'
import { SyncResolver } from 'jest-resolve'
const resolver: SyncResolver = (path, options) => {
const { defaultResolver } = options
try {
return defaultResolver(path, options)
} catch (e) {
const result = resolve.sync(options.basedir, path)
if (result) {
return result
} else {
throw e
}
}
}
export default resolver
As more options to solve this pile up here, I'd like to point out that using Jest to test code that has dependencies that have package imports (a.k.a subpath imports) should ideally just work, without doing any config or setting up resolvers.
Concur with @KermanX that this is not a feature request, but a bug.
Are FB engineers working on Jest, or are we solely waiting on a PR from the community?
@ottokruse community PR https://engineering.fb.com/2022/05/11/open-source/jest-openjs-foundation/
Fixed it for me by just switching from the default runner to https://github.com/nicolo-ribaudo/jest-light-runner, which disables most of Jest's “magic” features I don't need anyways.
I couldn't figure out how to use @BlackGlory script, so I did this:
resolver.cjs:
const resolve = require("enhanced-resolve");
const resolver = (path, options) => {
const { defaultResolver } = options;
try {
return defaultResolver(path, options);
} catch (e) {
const result = resolve.sync(options.basedir, path);
if (result) {
return result;
} else {
throw e;
}
}
};
module.exports = resolver;
jest.config.ts:
export default {
preset: "ts-jest/presets/default-esm",
globals: {
"ts-jest": {
useESM: true,
},
},
resolver: "./resolver.cjs",
};
To add to https://github.com/facebook/jest/issues/12270#issuecomment-1153458603 and https://github.com/facebook/jest/issues/12270#issuecomment-1166732265, here's an alternative implementation of the custom resolver which relies on the node:module core module instead of enhanced-resolve:
const nativeModule = require('node:module');
function resolver(module, options) {
const { basedir, defaultResolver } = options;
try {
return defaultResolver(module, options);
} catch (error) {
return nativeModule.createRequire(basedir).resolve(module);
}
}
module.exports = resolver;
Note: Since .resolve() already throws an error if the module cannot be resolved, there is no need to check its return value and rethrow the error caught from defaultResolver().
Presumably, at some point when Jest starts importing resolver as an ES module, the above can be rewritten as
import url from 'node:url';
export default async function resolver(path, options) {
const { basedir, defaultResolver } = options;
try {
return defaultResolver(path, options);
} catch (error) {
return await import.meta.resolve(path, url.pathToFileURL(basedir));
}
}
This relies on import.meta.resolve(), which at the time of this writing the latest version of Node 18 still requires --experimental-import-meta-resolve to use.
This relies on import.meta.resolve(), which at the time of this writing the latest version of Node 18 still requires --experimental-import-meta-resolve to use.
I have previously released a package resolve-esm to call import.meta.resolve() without the experimental flag.
This should work on all current Node.js LTS versions (v14, v16, v18).
I will be happy if this is of any help.
If helpful, until this issue is closed, the following works for the sole case of resolving chalk in a Jest-enabled Next.js project. No need to map the chalk reference itself:
moduleNameMapper: {
'#ansi-styles': 'ansi-styles/index.js',
'#supports-color': 'supports-color/index.js',
}
https://github.com/facebook/jest/releases/tag/v29.4.0
Can confirm this is working now, thank you! I can remove my resolver.cjs workaround now, yay!
This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. Please note this issue tracker is not a help forum. We recommend using StackOverflow or our discord channel for questions.