helmet icon indicating copy to clipboard operation
helmet copied to clipboard

Getting Error Type 'typeof import("/home/quophyie/projects/helmet-issue/node_modules/helmet/index")' has no call signatures when running tests with jest, ts-jest when using ESM / ECMAScript Modules

Open quophyie opened this issue 1 year ago • 12 comments

Hi

I am getting the error below when I run tests with jest, ts-jest where a module uses helmet

Type 'typeof import("/home/quophyie/projects/helmet-issue/node_modules/helmet/index")' 

I am using ESM (ECMAScript modules) and node version 19.9.0 and helmet v7.0.0 The weird thing is that, the app works fine when I just run it with ts-node

Here is my setup (The github repo is helmut-issue-441 ) node-version v19.9.0v

package.json

{
  "type": "module",
  "scripts": {
    "test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest -i --passWithNoTests",
    "start": "ts-node --esm --project tsconfig.json --transpile-only main.ts"
  },
  "dependencies": {
    "cross-env": "^7.0.3",
    "express": "^4.18.2",
    "helmet": "^7.0.0",
    "ts-node": "^10.9.1"
  },
  "devDependencies": {
    "@types/jest": "^29.5.5",
    "jest": "^29.7.0",
    "ts-jest": "^29.1.1",
    "typescript": "^5.2.2"
  }
}

tsconfig.json

{
  "compilerOptions": {
    "target": "es2022",
    "lib": ["es2022", "dom"],
    "typeRoots": ["./node_modules/@types", "src/types"],
    "allowJs": true,
    "allowImportingTsExtensions": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strictPropertyInitialization": false,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "resolveJsonModule": true,
    "isolatedModules": false,
    "jsx": "react",
    "pretty": true,
    "sourceMap": true,
    "noEmit": true,
    "downlevelIteration": true,
    "outDir": "./build",
    "rootDir": "."
  },
  "include": ["./**/*.ts", "declarations/**/*"],
  "exclude": ["build/**/*", "node_modules"]
}

main.test.ts

import {jest} from '@jest/globals'
import helmet from 'helmet'

helmet();

describe('Helmet Issue 441', () => {
    it('fails on call to helmut() function', () => {
        expect(true).toBe(true);
    });
});

main.ts

import helmet from 'helmet';

helmet();
console.log('This is OK');

quophyie avatar Oct 12 '23 09:10 quophyie

Strange...does this happen with any other modules, or just Helmet? Is ts-jest using the same TypeScript configuration as ts-node?

EvanHahn avatar Oct 12 '23 12:10 EvanHahn

Hi @EvanHahn Thanks for the quick response

This only happens with helmet module only. Other modules are OK

jest, ts-jest and ts-node all use the same typescript config .

here is the jest.config.ts with the embedded ts-jest config

jest.config.ts

import type { JestConfigWithTsJest } from 'ts-jest';

const jestConfig: JestConfigWithTsJest = {
    testFailureExitCode: 1,
    moduleFileExtensions: ['ts', 'js', 'json'],
    extensionsToTreatAsEsm: ['.ts'],
    preset: 'ts-jest/presets/default-esm',
    transform: {
        '^.+\\.(ts|tsx)$': [
            'ts-jest',
            {
                useESM: true,
                tsconfig: 'tsconfig.json',
            },
        ],
    },
    testMatch: [
        '**/*.test.(ts|js)',
        '**/*.spec.(ts|js)'
    ],
    testPathIgnorePatterns: [
        '<rootDir>/build',
        '<rootDir>/node_modules/'
    ],
    testEnvironment: 'node',
};

export default jestConfig;

ts-node also uses the exact same tsconfig.json

Here is the ts-node start script in the package.json

"start": "ts-node --esm --project tsconfig.json --transpile-only main.ts"

quophyie avatar Oct 12 '23 20:10 quophyie

Everything looks okay at a glance, but ESM + TypeScript + Jest often causes problems.

What's the full error you're seeing? What happens if you add // @ts-ignore before importing Helmet?

EvanHahn avatar Oct 12 '23 21:10 EvanHahn

@EvanHahn Here is the full console log

yarn run v1.22.19
warning package.json: No license field
$ cross-env NODE_OPTIONS=--experimental-vm-modules jest -i --passWithNoTests --detectOpenHandles --forceExit --runInBand
 FAIL  ./main.test.ts
  ● Test suite failed to run

    main.test.ts:4:1 - error TS2349: This expression is not callable.
      Type 'typeof import("/home/dman/projects/helmet-issue-441/node_modules/helmet/index")' has no call signatures.

    4 helmet();
      ~~~~~~

Test Suites: 1 failed, 1 total
Tests:       0 total
Snapshots:   0 total
Time:        0.912 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.

quophyie avatar Oct 13 '23 10:10 quophyie

Hi @EvanHahn I added the //@ts-ignore and that seems to have fixed it i.e. in my main.test.ts

import {jest} from '@jest/globals'
import helmet from 'helmet'

//@ts-ignore
helmet();

describe('Helmet Issue 441', () => {
    it('fails on call to helmut() function', () => {
        expect(true).toBe(true);
    });
});

Thanks for the help

Would you know why the //@ts-ignore solves the issue?

quophyie avatar Oct 13 '23 10:10 quophyie

It seems like ts-jest isn't pulling in the right type declarations for some reason. I don't know why.

// @ts-ignore simply tells TypeScript to ignore the next line. This is not a true fix, but a workaround for this problem.

Is there a way to see what type declaration file ts-jest is using? Maybe some verbose logging mode or something?

EvanHahn avatar Oct 14 '23 13:10 EvanHahn

I have the same problem. The type correctly points towards index.d.ts. However, the types are starting to behave correctly only if I do const helmet = require('helmet').default.

Sharcoux avatar Nov 11 '23 23:11 Sharcoux

Awhile ago, I chatted with a TypeScript team member who endorsed the way Helmet exports its types. But I concede that it's complicated and it's possible I made a mistake somewhere.

Is it possible that ts-jest (or some sub-system) is getting confused, trying to treat an ES module as a CommonJS one? Or something like that?

EvanHahn avatar Nov 12 '23 05:11 EvanHahn

when you use "moduleResolution": "node", it will work.

Any other resolution method will fail. I see this issue happening in the validator.js codebase too. I have to add in another default import.

When I change the moduleResolution to anything other than node helmet will not work.

{
  "compilerOptions": {
    "target": "ESNext",
    "lib": ["ESNext"],
    "module": "ESNext",
    "outDir": "./dist",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "allowImportingTsExtensions": true
  },
  "exclude": [
    "node_modules"
  ],
  "extends": "../../packages/tsconfig/base.json",
  "include": ["."]
}
// base.json
{
  "$schema": "https://json.schemastore.org/tsconfig",
  "display": "Default",
  "compilerOptions": {
    "composite": false,
    "declaration": true,
    "declarationMap": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "allowImportingTsExtensions": true,
    "noEmit": true,
    "inlineSources": false,
    "isolatedModules": true,
    "moduleResolution": "node",
    "noUnusedLocals": false,
    "noUnusedParameters": false,
    "preserveWatchOutput": true,
    "skipLibCheck": true,
    "strict": true,
    "strictNullChecks": true
  },
  "exclude": ["node_modules"]
}

// package.json (abbreviated)
{
  "name": "api",
  "version": "0.0.1",
  "private": true,
  "type": "module",
  "scripts": {
    "start": "node dist/index.js",
    "dev": "nodemon",
    "build": "tsup",
    "clean": "rimraf dist",
    "typecheck": "tsc --noEmit",
    "lint": "eslint src/",
    "test": "DOTENV_CONFIG_PATH=.env.test jest --detectOpenHandles"
  },
  "jest": {
    "preset": "@repo/jest-presets/jest/node",
    "setupFiles": [
      "dotenv/config"
    ],
    "globalSetup": "<rootDir>/test/global-setup.ts",
    "globalTeardown": "<rootDir>/test/global-teardown.ts",
    "setupFilesAfterEnv": [
      "<rootDir>/test/setup-file.ts"
    ]
  },
  "dependencies": {
    "express": "^4.18.2",
    "helmet": "^7.1.0",
  },
  "devDependencies": {
    "esbuild": "^0.19.7",
    "esbuild-register": "^3.5.0",
    "eslint": "*",
    "jest": "^29.7.0",
    "jest-fetch-mock": "^3.0.3",
    "nodemon": "^3.0.2",
    "supertest": "^6.3.3",
    "ts-node": "^10.9.1",
    "tsup": "^8.0.1",
    "typescript": "^5.3.3"
  }
}

I'm just using a vanilla express js setup with the use(helmet()) and import helmet from 'helmet'

This a turbo monorepo using pnpm but the TS-Config is working fine with other packages.

doing the: const helmet = require('helmet').default works when I change moduleResolution to ESNext only.

null-prophet avatar Dec 14 '23 01:12 null-prophet

To work with Node, I expect that "moduleResolution" needs to be set to "nodenext", "node10", or one of its aliases. According to the docs, you don't always get this by default.

EvanHahn avatar Dec 14 '23 14:12 EvanHahn

If I downgrade to 6.1.4, the error is gone. The 6.1.5 commit: https://github.com/helmetjs/helmet/commit/f8ae480dbb984134713ef19d3ef546b9d1ea1dc1

tbn-mm avatar Jan 29 '24 19:01 tbn-mm

@tbn-mm Could you create a sample project that reproduces your issue?

EvanHahn avatar Jan 29 '24 19:01 EvanHahn

There hasn't been anything actionable on this issue for months so I'm going to close. Please open a new issue if you run into any problems!

EvanHahn avatar Jun 01 '24 17:06 EvanHahn