jest icon indicating copy to clipboard operation
jest copied to clipboard

[Feature]: Support package imports in jest-resolve

Open anthony-redFox opened this issue 3 years ago • 16 comments

🚀 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

anthony-redFox avatar Jan 27 '22 17:01 anthony-redFox

Is there a current work around for the imports feature in jest?

renemroger avatar Jan 27 '22 21:01 renemroger

No, but once we have exports I guess imports shouldn't be too hard to build on top of it

SimenB avatar Feb 07 '22 00:02 SimenB

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.

kermanx avatar Feb 07 '22 07:02 kermanx

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

antongolub avatar Feb 10 '22 11:02 antongolub

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.

SimenB avatar Feb 10 '22 11:02 SimenB

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.

github-actions[bot] avatar Mar 12 '22 11:03 github-actions[bot]

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.

Twipped avatar Apr 07 '22 17:04 Twipped

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?

SimenB avatar Apr 07 '22 21:04 SimenB

~~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",
    ),
  },

jlarmstrongiv avatar Apr 27 '22 22:04 jlarmstrongiv

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

mizdra avatar Jun 13 '22 03:06 mizdra

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

BlackGlory avatar Jun 13 '22 04:06 BlackGlory

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 avatar Jun 13 '22 07:06 ottokruse

@ottokruse community PR https://engineering.fb.com/2022/05/11/open-source/jest-openjs-foundation/

jlarmstrongiv avatar Jun 13 '22 07:06 jlarmstrongiv

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.

Jaid avatar Jun 14 '22 13:06 Jaid

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

Maxim-Mazurok avatar Jun 27 '22 01:06 Maxim-Mazurok

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.

mxxk avatar Jul 25 '22 23:07 mxxk

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.

nujarum avatar Aug 28 '22 15:08 nujarum

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',
}

seanfuture avatar Oct 23 '22 17:10 seanfuture

https://github.com/facebook/jest/releases/tag/v29.4.0

SimenB avatar Jan 24 '23 10:01 SimenB

Can confirm this is working now, thank you! I can remove my resolver.cjs workaround now, yay!

Maxim-Mazurok avatar Feb 12 '23 01:02 Maxim-Mazurok

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.

github-actions[bot] avatar Mar 15 '23 00:03 github-actions[bot]