esbuild-jest icon indicating copy to clipboard operation
esbuild-jest copied to clipboard

TypeError: Property typeName of TSTypeReference expected node to be of a type ["TSEntityName"] but instead got "MemberExpression"

Open DreierF opened this issue 3 years ago • 17 comments

I tried replacing ts-jest in our project with esbuild-jest. However all tests that transitively reference the history library fail with:

TypeError: ....test.ts: Property typeName of TSTypeReference expected node to be of a type ["TSEntityName"] but instead got "MemberExpression"

I reduced it to a minimal example that passes without problems in ts-jest but fails with esbuild-jest:

import type { History } from 'history';

// Commenting the next line makes the test pass
type Alias = History;

test('bug', () => {
	// Removing one of the characters in the below comment makes the test pass
	// ock(
	console.log('Hello world!')
});

Both of the changes outlined in the comments make the test pass 🤨

Full sample can be found here: esbuild-jest-bug.zip

Output: (Click to expand)
❯ yarn test-ts-jest
yarn run v1.22.5
$ jest --config jest.config.ts-jest.js
 PASS  ./Sample.test.ts
  ✓ bug (15 ms)

  console.log
    Hello world!

      at Object.<anonymous> (Sample.test.ts:9:10)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.224 s, estimated 3 s
Ran all test suites.
✨  Done in 2.01s.

❯ yarn test-esbuild-jest
yarn run v1.22.5
$ jest --config jest.config.esbuild-jest.js
 FAIL  ./Sample.test.ts
  ● Test suite failed to run

    TypeError: .../esbuild-jest-bug/Sample.test.ts: Property typeName of TSTypeReference expected node to be of a type ["TSEntityName"] but instead got "MemberExpression"

      at Object.validate (node_modules/@babel/types/lib/definitions/utils.js:132:11)

Test Suites: 1 failed, 1 total
Tests:       0 total
Snapshots:   0 total
Time:        1.149 s
Ran all test suites.
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

DreierF avatar Feb 27 '21 17:02 DreierF

@DreierF are you using typescript with interface or type?

try to install "@babel/preset-typescript" then add this to your package.json or babel config

"babel": { "presets": [ "@babel/preset-typescript" ] }

aelbore avatar Feb 28 '21 03:02 aelbore

Not sure what you mean with "typescript with interface or type". Also I don't see why the Typescript preset should be necessary as esbuild can transpile ts way faster then babel can, which is the reason why I'm trying to get esbuild-jest to work in the first place. From looking over the code I found this line which sounds suspiciously related to the problem:

https://github.com/aelbore/esbuild-jest/blob/f80eb9900550f146f7e09e67bda3cea83ee60b19/src/index.ts#L33

This seems to be responsible for handling jest.mock calls, but is way underspecified. The original (non-minimal code) had a regular function there called block.

DreierF avatar Feb 28 '21 09:02 DreierF

On 0.4.0 everything works as expected

DreierF avatar Feb 28 '21 10:02 DreierF

@DreierF the above minimal example works fine for me, you only need to install babel presets typescript if you have jest.mocks because esbuild doesnt support jest.mocks thats why it needs to have babel jest to do it. by the way what version of esbuild-jest you are using?

if you are using [email protected] it only invoke or call babel-jest if you have jest.mock

what i meant is if you are using typescript with "type" and "interface" word meaning its reserve word of typescript

aelbore avatar Feb 28 '21 10:02 aelbore

Thanks! I got you now. I'm aware that type and interface are keywords in Typescript, but I thought ESBuild would strip them away during the transpilation to JS.

The thing is that I'm not using jest.mock at all, but esbuild-jest believes I do because the code contains the ock( fragment, which happens to appear at various places in the form of block() calls in my code, which in turn is a React Router API.

Version 0.4.0 of esbuild-jest did work without @babel/preset-typescript, but 0.5.0 needs @babel/preset-typescript. Would be nice if it wouldn't (as long as jest.mock is not used), but I'm ok with that. IMO it should be documented in the README, that @babel/preset-typescript is required now when using jest.mock and the heuristics could be made smarter to only detect actual jest.mock calls.

DreierF avatar Feb 28 '21 11:02 DreierF

@DreierF you only need @babel/preset-typescript if you are using jest.mock with typescript that uses "interface" or "type" if not you dont need it please see this repository https://github.com/aelbore/esbuild-jest-repro

and yes your right it should be documented i will update the README file thanks yup theres a TODO in this esbuild-jest i will use babel transformer to transform jest.mock into functions and then hoist it. it is currently in progress, if you are not using jest.mock i thin [email protected] is a stable one

aelbore avatar Feb 28 '21 11:02 aelbore

@DreierF if you are not using jest.mock [email protected] is pretty much stable :)

aelbore avatar Feb 28 '21 11:02 aelbore

@aelbore You shouldn't need @babel/preset-typescript even if you're using jest.mock with typescript. the babel parser can parse it just fine. This issue appears to be something else.

threepointone avatar Mar 01 '21 20:03 threepointone

Here's an additional anecdote: A test using jest.mock worked fine on my machine (Mac) but failed on CircleCI with the error above. Adding a babel.config.js with module.exports = {presets: ["@babel/preset-typescript"]}; fixed it on CircleCI. I'm on esbuild-jest@^0.5.0

danielberndt avatar Mar 09 '21 17:03 danielberndt

I'm using esbuild-jest@~0.5.0, and I get this issue when I run jest with the --coverage flag enabled.

jimmed avatar Mar 19 '21 23:03 jimmed

Have the same issue. Sadly. Isn't the usage of Babel anywhere in the pipeline basically cancels out esbuild benefits to 0? Did anyone measure an impact?

RIP21 avatar Mar 28 '21 17:03 RIP21

Ok, I see that it check whether there is ock( in the file and only then transpires it down using babel. So it shouldn't be that big but some gains will be there probably. @threepointone this hack is definitely good, but it seems that it doesn't work well with Typescript in all cases as I have exactly the same issue with the different snippet.

import { UrqlQueryResult, UrqlMutationResult } from '../useUrqlEnchanceHook'

const createState = (
  props?: Partial<UrqlQueryResult<any> | UrqlMutationResult<any, any>>,
): UrqlQueryResult<any> => {
  return {
    stale: false,
    fetching: false,
    error: undefined,
    data: null,
    ...props,
  }
}

test('test', () => {})

Doesn't fail.

Adding // jest.mock( Comment fails it So this snipped fails:

import { UrqlQueryResult, UrqlMutationResult } from '../useUrqlEnchanceHook'

// jest.mock('

const createState = (
  props?: Partial<UrqlQueryResult<any> | UrqlMutationResult<any, any>>,
): UrqlQueryResult<any> => {
  return {
    stale: false,
    fetching: false,
    error: undefined,
    data: null,
    ...props,
  }
}

test('test', () => {})

With this error:

  ● Test suite failed to run

    TypeError: /home/alos/Projects/LiveFlow/app-frontend/libraries/hooks/common/src/__tests__/useUrqlEnchanceHook.test.ts: Property typeName of TSTypeReference expected node to be of a type ["TSEntityName"] but instead got "MemberExpression"

      at Object.validate (../../../common/temp/node_modules/.pnpm/@babel/[email protected]/node_modules/@babel/types/lib/definitions/utils.js:132:11)
      at validateField (../../../common/temp/node_modules/.pnpm/@babel/[email protected]/node_modules/@babel/types/lib/validators/validate.js:24:9)
      at Object.validate (../../../common/temp/node_modules/.pnpm/@babel/[email protected]/node_modules/@babel/types/lib/validators/validate.js:17:3)
      at NodePath._replaceWith (../../../common/temp/node_modules/.pnpm/@babel/[email protected]/node_modules/@babel/traverse/lib/path/replacement.js:179:7)
      at NodePath.replaceWith (../../../common/temp/node_modules/.pnpm/@babel/[email protected]/node_modules/@babel/traverse/lib/path/replacement.js:161:8)
      at Object.ReferencedIdentifier (../../../common/temp/node_modules/.pnpm/@babel/[email protected]/node_modules/@babel/helper-module-transforms/lib/rewrite-live-references.js:179:14)
      at Object.newFn (../../../common/temp/node_modules/.pnpm/@babel/[email protected]/node_modules/@babel/traverse/lib/visitors.js:216:17)
      at NodePath._call (../../../common/temp/node_modules/.pnpm/@babel/[email protected]/node_modules/@babel/traverse/lib/path/context.js:55:20)
      at NodePath.call (../../../common/temp/node_modules/.pnpm/@babel/[email protected]/node_modules/@babel/traverse/lib/path/context.js:42:17)
      at NodePath.visit (../../../common/temp/node_modules/.pnpm/@babel/[email protected]/node_modules/@babel/traverse/lib/path/context.js:92:31)

So by looking at stack trace we can be sure that it's definitely related to Babel and it needs to be fixed somehow.

RIP21 avatar Mar 29 '21 15:03 RIP21

Managed to get rid of this error by moving babelTransform hack after the esbuild transform, which is, surely, destroys source maps for all the test files where there is jest.mock. If there are no mocks, then all good with them.

Like that:

            let result = esbuild.transformSync(sources.code, {
                loader,
                format: (options === null || options === void 0 ? void 0 : options.format) || 'cjs',
                target: (options === null || options === void 0 ? void 0 : options.target) || 'es2018',
                ...(options === null || options === void 0 ? void 0 : options.jsxFactory) ? {
                    jsxFactory: options.jsxFactory
                } : {
                },
                ...(options === null || options === void 0 ? void 0 : options.jsxFragment) ? {
                    jsxFragment: options.jsxFragment
                } : {
                },
                ...sourcemaps
            });
            if (sources.code.indexOf("ock(") >= 0 || (opts === null || opts === void 0 ? void 0 : opts.instrument)) {
                const source = require('./transformer').babelTransform({
                    sourceText: result.code,
                    sourcePath: filename,
                    config,
                    options: opts
                });
                result.code = source;
            }

RIP21 avatar Mar 29 '21 19:03 RIP21

So here is my lifehack to you folks. If you want to speedup Jest with esbuild, just don't, too many hacks to make mocks working :) Just use this https://github.com/alangpierce/sucrase/tree/main/integrations/jest-plugin instead.

Surcrase is mega quick and is similar to Babel and works no problem with Jest via this plugin. It will require you to hoist jest.mock calls yourself which is a bit annoying. But there is a PR opened that should fix that. https://github.com/alangpierce/sucrase/pull/540

Thanks for effort folks, but it's far from being stable :(

RIP21 avatar Mar 29 '21 19:03 RIP21

a workaround is to avoid named imports of types, so instead of

import { UrqlQueryResult, UrqlMutationResult } from '../useUrqlEnchanceHook'

do

import * as UseUrqlEnchanceHook from '../useUrqlEnchanceHook'

and qualify the usages.

It looks like the source of the failure is here in @babel/helper-module-transforms, where a ReferencedIdentifier is matched and replaced with a MemberExpression (computed by buildImportReference). Typescript type annotations match ReferencedIdentifier and are mistakenly replaced. I filed a bug against Babel.

It is pretty weird that the string ock( triggers this; I stumbled across it with a method named isBlock.

jaked avatar Apr 02 '21 23:04 jaked

The issue with ock( was introduced in this PR: https://github.com/aelbore/esbuild-jest/pull/20/files#diff-a2a171449d862fe29692ce031981047d7ab755ae7f84c707aef80701b3ea0c80R33

felipero avatar Jun 09 '21 05:06 felipero

Have the same issue. Sadly. Isn't the usage of Babel anywhere in the pipeline basically cancels out esbuild benefits to 0? Did anyone measure an impact?

kind of hard to avoid when jest depends on babel core itself, feels bad.

Codex- avatar Sep 03 '21 03:09 Codex-