ts-jest: TS imports with .js extension break Jest-resolve

Issue :

TypeScript files using web-compatible ES6 imports cause Jest to throw an error about not being able to find the module. This error comes from jest-resolve/build/index.js:229 and happens regardless of the module type specified in ts-jest (commonjs, amd, ES6, etc.)

Expected behavior :

TypeScript supports adding a .js extension to import statements for web-compatibility (since imports require a full path, including the extension).

I would expect ts-jest to allow TypeScript to handle this and convert it to commonjs before it gets passed to jest.

Minimal repo :

/* user.ts */
export class User {
  public name : string;

  constructor(name : string) {
    this.name = name;
  }
}
/* user.spec.ts */
import { User } from './user.js';

describe('User', function() {
});

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 32
  • Comments: 22

Commits related to this issue

Most upvoted comments

I got things working with .js extensions in typescript imports with the following jest.config.js:

/**  @type {import('@jest/types').Config.ProjectConfig} */
const config = {
  transform: {
    "\\.[jt]sx?$": "ts-jest",
  },
  "globals": {
    "ts-jest": {
      "useESM": true
    }
  },
  moduleNameMapper: {
    "(.+)\\.js": "$1"
  },
  extensionsToTreatAsEsm: [".ts"],
};

export default config;

I set up a custom resolver and just published it to npm: jest-ts-webcompat-resolver

@justrhysism you must double the backslashes:

    "moduleNameMapper: {
        "^(\\.\\.?\\/.+)\\.jsx?$": "$1"
    }

Hello,

I want to share my config as well as far as the solution from above didn’t work out for me. I’m using nodejs v18.12.0, jest 29.2.2 and ts-jest 29.0.3, typescript 4.8.4.

I have "type": "module", in my package.json.

My tsconfig.json looks like this:

{
  "compilerOptions": {
    "outDir": "./lib/",
    "inlineSourceMap": true,
    "target": "es2022",
    "lib": ["ES2022"],
    "module": "ESNext",
    "moduleResolution": "NodeNext",
    "esModuleInterop": true,
    // "allowSyntheticDefaultImports": true, // this one along with allowJs are not required in my project
    "noImplicitAny": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["./test/**/*.ts", "./src/**/*.ts"]
}

Jest config file jest.config.cjs looks like this:

/** @type {import('ts-jest').JestConfigWithTsJest} */
const jestConfig = {
  testMatch: ['<rootDir>/test/?(*.)+(spec|test).ts'],
  transform: { '\\.[jt]s?$': ['ts-jest', { tsconfig: { allowJs: true } }] },  // allowJs is required for get-port
  transformIgnorePatterns: ['node_modules/(?!get-port/.*)'],  // you might need to ignore some packages
  moduleNameMapper: {
    '^(\\.{1,2}/.*)\\.[jt]s$': '$1',
  },
  // ...
}

module.exports = jestConfig

This comment started me on the right path, but for my project, the solution is simpler: make sure the moduleNameMapper only maps the ‘.ts’ files that you refer to as .js in your imports elsewhere. In my case, those were the ./[filename].ts files, which are imported as ./[filename].js but for jest need to be imported as ./[filename].

In jest.config.json syntax:

"moduleNameMapper": {
    "^(\\.\\/.+)\\.js$": "$1"
  },

custom resolver doesn’t belong to ts-jest.

@ahnpnl In my very humble opinion, if someone gives a Jest plugin for people to write .test.ts files instead of .test.js files, but it doesn’t work with default functionality of TypeScript, then that’s the plugin author’s problem, not Jest’s.

.js extensions for module specifiers are a default out-of-the-box feature of TypeScript.

I got things working with .js extensions in typescript imports with the following jest.config.js:

/**  @type {import('@jest/types').Config.ProjectConfig} */
const config = {
  transform: {
    "\\.[jt]sx?$": "ts-jest",
  },
  "globals": {
    "ts-jest": {
      "useESM": true
    }
  },
  moduleNameMapper: {
    "(.+)\\.js": "$1"
  },
  extensionsToTreatAsEsm: [".ts"],
};

export default config;

Since the global ts-jest config is considered as deprecated.

I adjust the good tips by this way

{
  "transform": {
    "\\.[jt]s?$": [
      "ts-jest",
      {
        "useESM": true
      }
    ]
  },
  "moduleNameMapper": {
    "(.+)\\.js": "$1"
  },
  "extensionsToTreatAsEsm": [
    ".ts"
  ],
  ...
}

I tweaked the above version to allow ../ relative imports also:

"moduleNameMapper: {
    "^(\.\.?\/.+)\.jsx?$": "$1"
}

I set up a custom resolver and just published it to npm: jest-ts-webcompat-resolver

Won’t work for TSX files, but changing a bit the code to also tries resolving path replacing .js to .tsx seems to work fine.

function getResolver() {
  try {
    return require('jest-resolve/build/defaultResolver').default;
  } catch (_) {
    return require('jest-resolve/build/default_resolver').default;
  }
}

const JAVASCRIPT_EXTENSION = /\.js$/i;

function resolverTSAndTSX(path, options) {
  const resolver = options.defaultResolver || getResolver();

  try {
    return resolver(path, options);
  } catch (error) {
    if (!JAVASCRIPT_EXTENSION.test(path)) throw error;

    try {
      return resolver(path.replace(JAVASCRIPT_EXTENSION, '.ts'), options);
    } catch (_) {
      return resolver(path.replace(JAVASCRIPT_EXTENSION, '.tsx'), options);
    }
  }
}

module.exports = resolverTSAndTSX;

I’ll made a PR tomorrow, but this should be included in ts-jest for sure.

EDIT: I’ve create a package to resolve imports same way TS does with import paths that has “.js” extension. https://github.com/VitorLuizC/ts-jest-resolver

@kulshekhar this has been closed for awhile but the issue still happen in ts-jest 26.5.4.

Since handling “.js” extensions in import statement is standard in TypeScript (and will become increasingly needed as Node.js moves towards ES modules), shouldn’t ts-jest resolve this correctly without the need for @dpogue custom resolver?

This is Jest runtime error, not ts-jest type checking error. There are a few things to check:

  • Make sure the file has module.exports
  • Make sure allowJs is true.
  • Make sure jest config transform value specifying js processed by ts-jest

When transforming ts to js, the code from "./types.js"; becomes require(“./types.js”), which is a valid syntax. However, Jest resolver doesn’t understand, probably because missing module.exports

A jest-resolve error:

 FAIL  test/index.spec.js
  ● Test suite failed to run

    Cannot find module './types.js' from 'src/index.ts'

    Require stack:
      src/index.ts
      test/index.spec.js

    > 1 | import { MyType } from "./types.js";
        | ^
      2 | 

      at Resolver.resolveModule (node_modules/jest-resolve/build/index.js:306:11)
      at Object.<anonymous> (src/index.ts:1:1)

importing ./types.js (./src/types.ts file exporting some types/enums declarations) from ./src/index.ts.

ts-jest doesn’t do anything with Jest module resolution. The job of ts-jest is transforming ts to js. Transforming means compiling. Compiling process doesn’t trigger the codes itself.

The output of import { MyType } from "./types.js"; is

var globals_1 = require(\\"./types.js\\");

This is a correct js compiling. However, how that require(\\"./types.js\\") executes depending on Jest.

As I said above, ts-jest is a Jest transformer, please check https://jestjs.io/docs/next/code-transformation to understand more.

custom resolver doesn’t belong to ts-jest. ts-jest is just a Jest transformer to transform ts to js and it doesn’t do anything with the way how Jest resolves a file. ts-jest can add a section to documentation regarding to this error but natively, it is not ts-jest issue so the resolver should stay on its own, not included in ts-jest

The issue is mainly caused by when using js extension, Node treats it as CommonJS, if you don’t have module.exports it is not a valid js to Node to import.

It is important to distinguish between runtime error vs type checking error. Runtime errors are from Jest and type checking errors are from ts-jest.

Related issue at Storybook: https://github.com/storybookjs/storybook/issues/15962 Both Jest and Storybook need to build code from sources, but might encounter index.ts “barrel files” exporting files using .js extension when a librairy is meant to be exposed as ESM.

When not using ts-jest, this issue could help: https://github.com/swc-project/jest/issues/64#issuecomment-1029753225