Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cannot find module '@middy/core' when running jest with typescript and esm #1148

Closed
Heinan-Phau opened this issue Dec 3, 2023 · 15 comments
Closed

Comments

@Heinan-Phau
Copy link

Describe the bug
hello I am running typescript with esm and jest and I have a problem with middy:

I had this test:

import { APIGatewayEvent, Context } from 'aws-lambda';
import { lambda } from '../index.js';

describe('get-by-id handler', () => {
    test('Should not find id', async () => {
        const x = jest.fn();
        x();
        expect(x).toHaveBeenCalled();
    });
});

which worked fine with the configurations I supplied in the "To Reproduce section" and npm run test command.
howerver when I changed it to look like this:

import { APIGatewayEvent, Context } from 'aws-lambda';
import { lambda } from '../index.js';

describe('get-by-id handler', () => {
    test('Should not find id', async () => {
        const res = await lambda({
            pathParameters: { id: 'id' },
        } as unknown as APIGatewayEvent, {} as Context);
        console.log(res);

        const x = jest.fn();
        x();
        expect(x).toHaveBeenCalled();
    });
});

I get this error

 jest -c jest.config.cjs

 FAIL  app/src/handlers/get-by-id/tests/index.test.ts
  ● Test suite failed to run

    app/src/handlers/get-by-id/index.ts:4:19 - error TS2307: Cannot find module '@middy/core' or its corresponding type declarations.

    4 import middy from '@middy/core';
                        ~~~~~~~~~~~~~

Test Suites: 1 failed, 1 total
Tests:       0 total
Snapshots:   0 total
Time:        11.409 s, estimated 13 s
Ran all test suites.

To Reproduce
How to reproduce the behaviour:
tsconfig.json:

{
  "extends": "./node_modules/@tsconfig/node20/tsconfig.json",
  "compilerOptions": {
    "declaration": true,
    "outDir": "./dist/",
    "noUnusedParameters": false,
    "noPropertyAccessFromIndexSignature": false,
    "resolveJsonModule": true
  },
}

jest.config.cjs

module.exports = {
    preset: 'ts-jest',
    testEnvironment: 'node',
    testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(ts?)$',
    moduleNameMapper: {
        '^(\\.{1,2}/.*)\\.js$': '$1',
    },
};

package.json:

{
  "type": "module",
  "main": "index.js",
  "scripts": {
    "build": "npm run clean && npm run copy-files && npm run tsc",
    "copy-files": "copyfiles -e \"!{*.md,package?(-lock).json}\" \"**/*.*\" dist",
    "clean": "rimraf ./dist",
    "tsc": "tsc -p tsconfig.build.json",
    "deploy": "cdktf deploy",
    "lint": "npx eslint . --ext .ts,.tsx",
    "test": "jest -c jest.config.cjs",
    "coverage": "jest -c jest-cov.config.cjs"
  },
  "license": "ISC",
  "devDependencies": {
    "@cdktf/provider-aws": "^18.0.6",
    "@tsconfig/node20": "^20.1.2",
    "@types/aws-lambda": "^8.10.129",
    "@types/jest": "^29.5.10",
    "@types/node": "^20.10.0",
    "@typescript-eslint/eslint-plugin": "^6.13.0",
    "@typescript-eslint/parser": "^6.13.0",
    "cdktf": "^0.19.1",
    "constructs": "^10.3.0",
    "copyfiles": "^2.4.1",
    "eslint": "^8.54.0",
    "eslint-config-airbnb-base": "^15.0.0",
    "eslint-config-airbnb-typescript": "^17.1.0",
    "eslint-plugin-import": "^2.29.0",
    "eslint-plugin-require-extensions": "^0.1.3",
    "jest": "^29.7.0",
    "rimraf": "^5.0.5",
    "ts-jest": "^29.1.1",
    "ts-node": "^10.9.1",
    "typescript": "^5.3.2"
  },
  "dependencies": {
    "@aws-sdk/client-dynamodb": "^3.460.0",
    "@aws-sdk/client-secrets-manager": "^3.460.0",
    "@aws-sdk/lib-dynamodb": "^3.460.0",
    "@middy/core": "^5.0.3",
    "axios": "^1.6.2",
    "dotenv": "^16.3.1"
  }
}

handler:

import type { APIGatewayEvent } from 'aws-lambda';
import middy from '@middy/core';
const handler = async (event: APIGatewayEvent) => {
    return {
        statusCode: 200,
    };
};

// eslint-disable-next-line import/prefer-default-export
export const lambda = middy().handler(handler);

Expected behaviour
I would expect the test to pass when I run npm run test

Environment (please complete the following information):

  • Node.js: [20.9.0]
  • Middy: [5.0.3]
  • AWS SDK [3.460.0]

Additional context
what I have tried so far step by step:

  1. changing test script in packge.json to this
    "NODE_OPTIONS=--experimental-vm-modules jest -c jest.config.cjs"
    as mentioned here
    result was the same

  2. changing jest.config.cjs to this:

module.exports = {
    preset: 'ts-jest/presets/default-esm',
    testEnvironment: 'node',
    testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(ts?)$',
    moduleNameMapper: {
        '^(\\.{1,2}/.*)\\.js$': '$1',
    },
    transform: {
        // '^.+\\.[tj]sx?$' to process js/ts with `ts-jest`
        // '^.+\\.m?[tj]sx?$' to process js/ts/mjs/mts with `ts-jest`
        '^.+\\.tsx?$': [
            'ts-jest',
            {
                useESM: true,
            },
        ],
    },
};

as mentioned here
result was the same

  1. changing jest.config.cjs to this:
const esModules = ["@middy"].join("|")
module.exports = {
    preset: 'ts-jest/presets/default-esm',
    testEnvironment: 'node',
    testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(ts?)$',
    moduleNameMapper: {
        '^(\\.{1,2}/.*)\\.js$': '$1',
    },
    transform: {
        '^.+\.ts?$': [
            'ts-jest',
            {
                useESM: true,
            },
        ],
    },
    transformIgnorePatterns: [`node_modules/(?!${esModules})`],
};

as mentioned here
result was the same

  1. changing tsconfig.json to this:
{
  "compilerOptions": {
    "incremental": true,
    "target": "es2020",
    "module": "es2020",
    "declaration": true,
    "sourceMap": true,
    "composite": true,
    "strict": true,
    "moduleResolution": "node",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "preserveConstEnums": true,
    "resolveJsonModule": true,
    "allowJs": true,
    "rootDir": ".",
    "outDir": "lib"
  },
  "include": ["src/**/*", "tests/**/*"],
  "exclude": ["node_modules"]
}

as mentioned here
got this error:

ReferenceError: jest is not defined

       9 |         console.log(res);
      10 |
    > 11 |         const x = jest.fn();
         |                   ^
      12 |         x();
      13 |         expect(x).toHaveBeenCalled();
      14 |     });

      at Object.<anonymous> (app/src/handlers/get-by-id/tests/index.test.ts:11:19)
@Audrey-Ann
Copy link

**I am experiencing the same issue! ⬆️ 😞 **

When running:

npm run test 

Where:

  "scripts": {
    [...]
    "test": "NODE_OPTIONS=--experimental-vm-modules jest --no-cache",
  },

Dependencies:

  • "@middy/core": "^5.1.0"
  • "typescript": "^5.2.2"
  • "ts-jest": "^29.1.1"
  • "ts-node": "^10.9.1"
  • "jest": "^29.7.0",
  • "@types/jest": "^29.5.7",
  • "ts-jest": "^29.1.1",

My jest.config.ts:

import { JestConfigWithTsJest } from 'ts-jest';

const esModules = ['@middy'].join('|');

const config: JestConfigWithTsJest = {
  verbose: true,
  moduleFileExtensions: ['js', 'json', 'ts'],
  rootDir: 'src',
  testRegex: '.*\\.spec\\.ts$',
  collectCoverageFrom: ['**/*.(t|j)s'],
  transform: { '^.+\\.(t|j)s$': ['ts-jest', { useESM: true }] },
  transformIgnorePatterns: [`node_modules/(?!${esModules})`],
  testEnvironment: 'node',
  moduleNameMapper: { '^~/(.+)$': '<rootDir>/$1' },
  moduleDirectories: ['node_modules', 'src'],
};

export default config;

My tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ES2022",
    "lib": ["ES2022"],
    "allowJs": true,
    "isolatedModules": true,
    "strict": true,
    "moduleResolution": "node",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "baseUrl": "./",
    "paths": { "~/*": ["./src/*"] },
    "allowSyntheticDefaultImports": true,
    "forceConsistentCasingInFileNames": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "resolveJsonModule": true,
    "typeRoots": ["node_modules/@types", "./src/shared/types"]
  },
  "include": ["src", "./.eslintrc.js", "./jest.config.ts"],
  "exclude": ["node_modules", "dist"]
}

Notes:

@TheHolyWaffle
Copy link

This is blocking us from upgrading to v5. We don't have a wildly exotic jest.config.js:

module.exports = {
  setupFilesAfterEnv: ['<rootDir>/setup-jest.js'],
  coverageReporters: ["clover", "json", "lcov", "text", "html", "cobertura","text-summary"],
  transform: {
    "^.+\\.(t|j)sx?$": "@swc/jest"
  },
  collectCoverageFrom: [
    "src/**",
    "!**/*.builders.ts",
    "!**/*-dev.client.ts",
  ],
};

@drmikecrowe
Copy link

I've spent days fighting this thinking it was my setup. This is really critical

@willfarrell
Copy link
Member

The ESM part of the middy package hasn't changed. Based on the configs above, CJS isn't supposed to be used. A quick search on StackOverflow shows this kind of error is very common for jest. I found this pinned on the jest project: jestjs/jest#9430 (Native support for ES Modules).

@Audrey-Ann
Copy link

Thanks @willfarrell I will look into the link you provided. However, the issue I had was specific to @middy.. In other words, all other dependencies using ESM are working. Can you validate if, following one of the setups we described above (with the provided dependencies versions), it works? If it does, can you share with us what’s missing or not working properly. As you can see, this issue seems to happen to many people who have tried and followed the middy documentation already.

(I will still investigate the link you provided next week. Hoping and find an information that could be missing to allow Jest to find the @middy module… and share my finding here)

@willfarrell
Copy link
Member

I personally don't use jest or typescript. Any documentation on these has been mostly community provided. Sorry I can't be more help here.

(I will still investigate the link you provided next week. Hoping and find an information that could be missing to allow Jest to find the @middy module… and share my finding here)

Appreciated, a PR is most welcome.

@drmikecrowe
Copy link

drmikecrowe commented Dec 10, 2023

@willfarrell -- apologies for piling on, that was not my intent. I think the takeaway is that though ESM should be universal, packages like jest are throwing a wrench in their adoption.

I've tried switching over jest to ESM only, and I'm past the middy error but now fighting other packages that aren't compatible with that mode. Example:

Error: Cannot find package '@aws-sdk/client-ssm' imported from /workspace/node_modules/.pnpm/@middy+ssm@5.1.0/node_modules/@middy/ssm/index.js

@willfarrell
Copy link
Member

It's all good, apologies not necessary. We anticipated non-runtime packages (build, test, etc) might have issues. The struggle to maintain CJS with all the edge cases they create was one of the reason we chose to drop support. We know they will natively support ESM one day soon, the whole industry is moving towards it. Please let them know it's important to natively support ESM. In the meantime v4.x is super stable to use while you wait for jest and others to upgrade.

@minhhuynh-smg
Copy link

Please try this, it works for my case.

In the jest.config.js:

transformIgnorePatterns: ['/node_modules/(?!(@middy/core)/)'],
moduleNameMapper: {
   '^@middy/core$': '<rootDir>/node_modules/@middy/core',
}

@ceberttylertech
Copy link

I've followed all the suggestions listed here and am also still having the same issue after upgrading from Middy 4 to 5.

@drmikecrowe
Copy link

For reference, I downgraded the version to 4. I'm also using yarn PNP, so that made things more difficult

@Heinan-Phau
Copy link
Author

Please try this, it works for my case.

In the jest.config.js:

transformIgnorePatterns: ['/node_modules/(?!(@middy/core)/)'],
moduleNameMapper: {
   '^@middy/core$': '<rootDir>/node_modules/@middy/core',
}

yup doesn't work for me either. maybe you can give the whole setup?

@mmuller88
Copy link

@minhhuynh-smg thanks that worked for me. Here is my whole config:

module.exports = {
  preset: "ts-jest",
  testEnvironment: "node",
  transform: {
    "^.+\\.(ts|tsx|js)$": "ts-jest",
  },
  transformIgnorePatterns: [
    "/node_modules/(?!(@middy/core|@middy/http-error-handler|@middy/util)/)",
  ],
  moduleNameMapper: {
    "^@middy/core$": "<rootDir>/node_modules/@middy/core",
    "^@middy/util$": "<rootDir>/node_modules/@middy/util",
    "^@middy/http-error-handler$":
      "<rootDir>/node_modules/@middy/http-error-handler",
  },
};

@willfarrell
Copy link
Member

Would someone like to update the docs to reflect what's missing from the docs?

https://github.com/middyjs/middy/blob/main/website/docs/intro/06-testing.md#jest-and-typescript

@jrobens
Copy link

jrobens commented Mar 14, 2024

The docs as described in the middy files didn't quite help me.

The biggest impact was NODE_OPTIONS=--experimental-vm-modules, although this causes another set of issues.

The simplest middy + jest setup I could create https://github.com/jrobens/middyskel. Not very 'deterministic' as there are a lot of moving parts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

No branches or pull requests

9 participants