helmetjs / helmet

Help secure Express apps with various HTTP headers
https://helmetjs.github.io/
MIT License
10.24k stars 369 forks source link

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 #441

Closed quophyie closed 5 months ago

quophyie commented 1 year ago

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');
EvanHahn commented 1 year ago

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

quophyie commented 1 year ago

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"
EvanHahn commented 1 year ago

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?

quophyie commented 1 year ago

@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 commented 1 year ago

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?

EvanHahn commented 1 year ago

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?

Sharcoux commented 12 months ago

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.

EvanHahn commented 12 months ago

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?

null-prophet commented 10 months ago

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.

EvanHahn commented 10 months ago

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.

tbn-mm commented 9 months ago

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

EvanHahn commented 9 months ago

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

EvanHahn commented 5 months ago

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!