svelteness / svelte-jester

A Jest transformer for Svelte - compile your components before importing them into tests.
MIT License
128 stars 18 forks source link

Unable to get Typescript working with many ES modules #16

Open therealpaulgg opened 4 years ago

therealpaulgg commented 4 years ago

I have a typescript Svelte component that makes use of moment.JS, importing like so:

import moment from "moment

It works with my rollup compilation and development server flawlessly. However, when using svelte-jester and using preprocessing, it says that 'moment is not a function'.

Here is my jest configuration:

module.exports = {
    testEnvironment: "jsdom",
    transform: {
        "^.+\\.svelte$": [
            "svelte-jester",
            {
                preprocess: true
            }
        ],
        "^.+\\.js$": "babel-jest",
        "^.+\\.ts$": "ts-jest"
    },
    moduleFileExtensions: ["js", "ts", "svelte"],
    setupFilesAfterEnv: ["./jestSetup.ts"],
    transformIgnorePatterns: ["node_modules/(?!(svelte-typewriter|svelte-flatpickr)/)"],
    moduleNameMapper: {
        "\\.(css|less|scss)$": "identity-obj-proxy"
    },
    testPathIgnorePatterns: ["/lib/", "/node_modules/"]
}

jestSetup.ts:

import "@testing-library/jest-dom";

tsconfig.json (have tried many variants):

{
    "extends": "@tsconfig/svelte/tsconfig.json",
    "include": ["src/**/*", "src/types/*"],
    "exclude": ["node_modules/*", "__sapper__/*", "public/*"],
    "compilerOptions": {
        "types": ["jest", "node"],
        "allowSyntheticDefaultImports": true,
        "target": "ES2019"
    }
}

test output:


    TypeError: moment is not a function

      35 |                 dv[0] = flatpickr.formatDate(dv[0], "Y-m-d")
      36 |             }
    > 37 |             if (
         |             ^
      38 |                 typeof dv[1] != "string" &&
      39 |                 String(new Date(dv[1])) !== "Invalid Date"
      40 |             ) {

      at updateDate (src/components/Options.svelte:37:13)
      at Object.$$self.$$.update (src/components/Options.svelte:26:4)
      at init (node_modules/svelte/internal/index.js:1450:8)
      at new Options (src/components/Options.svelte:1815:3)
      at Array.create_default_slot_5 (src/App.svelte:25:12)
      at create_slot (node_modules/svelte/internal/index.js:65:29)
      at create_fragment (src/components/Sidebar.svelte:19:23)
      at init (node_modules/svelte/internal/index.js:1454:37)
      at new Sidebar (src/components/Sidebar.svelte:122:3)
      at create_fragment (src/App.svelte:377:12)
      at init (node_modules/svelte/internal/index.js:1454:37)
      at new App (src/App.svelte:510:3)
      at Object.render (node_modules/@testing-library/svelte/dist/pure.js:71:21)
      at Object.<anonymous> (tests/integration/Home.spec.ts:5:40)

This isn't the first time I have had trouble with transpiling. There have been issues with getting other Svelte components to work, problems with flatpickr, etc.

Please let me know if there is something I am doing wrong.

therealpaulgg commented 4 years ago

I was able to get it to work by doing import * as moment from "moment" in Jest, but that makes my application not work. So clearly something is going on with the transpilation.

mihar-22 commented 4 years ago

Hey @therealpaulgg these types of issues come up a lot but they're always related to Jest, generally with how Jest resolves modules and transforms them. Jest only uses svelte-jester when it finds a file it doesn't understand such as .svelte and it then usessvelte-jester to transform that code to JS, but Jest is still responsible for resolving modules. By default Jest only resolves CommonJS by looking at the main key in package.json, and it's why import * as moment from "moment" works, and it's also why it fails resolving modules fails a lot.

Anyway ... that was to help you potentially debug in the future but in your case you're using TypeScript to resolve modules so ... the first thing I'd say is you can remove babel-jest and let ts-jest resolve modules:

transform: {
  // ...
  "^.+\\.(js|ts)$": "ts-jest"
}

The second is you should configure TypeScript to use the node module resolution strategy, there's a great section in the handbook about it.

"compilerOptions": {
  // ...
 "moduleResolution": "node"
}

Try these changes, if it doesn't still work let me know if you're receiving any new errors or the same.

therealpaulgg commented 4 years ago

Still having the same issue. I've changed my config (removed the extends svelte tsconfig portion, wasnt sure if that was messing with compilerOptions):

tsconfig.json:

{
    "include": ["src/**/*", "src/types/*"],
    "exclude": ["node_modules/*", "__sapper__/*", "public/*"],
    "compilerOptions": {
        "types": ["jest", "svelte"],
        "allowSyntheticDefaultImports": true,
        "target": "ES2019",
        "moduleResolution": "node",
        "allowJs": true,
        "importsNotUsedAsValues": "error",
        "isolatedModules": true,
        "sourceMap": true,
        "esModuleInterop": true,
        "skipLibCheck": true,
        "forceConsistentCasingInFileNames": true
    }
}

jest.config.js:

module.exports = {
    preset: "ts-jest",
    testEnvironment: "jsdom",
    transform: {
        "^.+\\.svelte$": [
            "svelte-jester",
            {
                preprocess: true
            }
        ],
        "^.+\\.(js|ts)$": "ts-jest"
    },
    moduleFileExtensions: ["js", "ts", "svelte"],
    setupFilesAfterEnv: ["./jestSetup.ts"],
    transformIgnorePatterns: [
        "node_modules/(?!(svelte-typewriter|svelte-flatpickr)/)"
    ],
    moduleNameMapper: {
        "\\.(css|less|scss)$": "identity-obj-proxy"
    },
    globals: {
        "ts-jest": {
            isolatedModules: true
        }
    },
    testPathIgnorePatterns: ["/lib/", "/node_modules/"]
}

Also worth noting I guess, since it's weird, I am using flatpickr as well for something using the format import flatpickr from "flatpickr". Tried doing import * as moment from "moment", so that stage of jest clears. and then I get (when doing flatpickr.parseDate(...)):

TypeError: Cannot read property 'parseDate' of undefined
mihar-22 commented 4 years ago

Oh there's few more steps to configure ts-jest, add allowJs: "true" to your tsconfig compilerOptions and then set the preset in jest.config.js to the following ts-jest/presets/js-with-ts. Let me know if it's still the same after that.

mihar-22 commented 4 years ago

If you also want the original Svelte files when using Jest then it's a good idea to use jest-svelte-resolver.

therealpaulgg commented 4 years ago

Same deal 😕

also not totally how to use jest-svelte-resolver but it gave me an error basically immediately when parsing file:

export { default as Router } from "./Router.svelte";
    ^^^^^^

    SyntaxError: Unexpected token 'export
mihar-22 commented 4 years ago

That really sucks @therealpaulgg, so we don't go back and forth too much, when I have some time today I'll set up a project just like yours, and I'll see if I can get it working.

mihar-22 commented 4 years ago

Hey @therealpaulgg I tried to recreate the issue but I can't. Everything works on my end. Can you give me an example of some file where you're trying the things that are failing? Some demo Svelte component with whatever you're importing and using would be awesome.

therealpaulgg commented 4 years ago

Yes in fact I can send you the entire project setup I'm using...

My test in test/integration is rather 'dumb' so to speak and doesn't really do a whole lot. It loads the entire App.svelte obviously to make sure everything works. I just added the DatePicker.svelte component which imports moment and flatpickr and things like that. There shouldn't be any import errors, but in its current state I doubt the test will pass, just kind of threw the code in there. As long as I can get to a point where the error isn't related to bad imports, that's great.

https://github.com/therealpaulgg/svelte-rollup-template

mihar-22 commented 4 years ago

Okay so first thing we now know is that moment has issues with Jest resolution, not sure why ... There is this hack to make it work. I'd like to recommend this library as an alternative: https://github.com/iamkun/dayjs. Same API but only 2KB.

Now need to figure out why Typewriter.svelte is not getting transformed.

mihar-22 commented 4 years ago

So Jest always resolves based on the main key in the package.json and that's why svelte-flatpickr is resolved as a CJS module and it only works if we do import * as Flatpickr from 'svelte-flatpickr. You can always open up node_modules/{package}/package.json and have a look at the exports to see what's being resolved. If you look inside node_modules/svelte-flatpickr/package.json you'll see there is both a main and svelte export. Jest is defaulting to main. If we want the svelte file then we have to use a resolver like the one I linked earlier. Simply install it and add this line "resolver": "jest-svelte-resolver" in jest.config.js.

mihar-22 commented 4 years ago

Finally, Typewriter was failing because you added node_modules to testPathIgnorePatterns in jest.config.js. This means Jest won't transform the file and ignores it, then at runtime it will fail because it doesn't understand .svelte files.

mihar-22 commented 4 years ago

Overall here's what you can do:

  1. Run npm install @tsconfig/svelte and change your tsconfig to:
{
  "extends": "@tsconfig/svelte/tsconfig.json",
  "include": ["src/**"],
  "exclude": ["node_modules/**"],
  "compilerOptions": {
      "types": ["jest"],
      "allowJs": true,
      "allowSyntheticDefaultImports": true,
  }
}
  1. Change your jest.config.js to:
module.exports = {
    preset: "ts-jest/presets/js-with-ts",
    globals: {
      'ts-jest': {
         // Let's avoid type checking during tests (performance boost).
        diagnostics: false
      }
    },
    transform: {
        "^.+\\.svelte": [
            "svelte-jester",
            {
                preprocess: true
            }
        ],
        "^.+\\.(js|ts)": "ts-jest"
    },
    moduleFileExtensions: ["svelte", "js", "ts"],
    setupFilesAfterEnv: ["./jestSetup.ts"],
    transformIgnorePatterns: [
        "node_modules/(?!svelte-typewriter|svelte-flatpickr)/",
    ],
    moduleNameMapper: {
        "\\.(css|less|scss)$": "identity-obj-proxy"
    }
}
  1. Swap moment for dayjs.

  2. Do import * as Flatpickr from 'svelte-flatpickr or setup jest-resolver and do import Flatpickr from 'svelte-flatpickr.

therealpaulgg commented 4 years ago

Ok, thanks! tried all that. I am now getting dayjs is not a function. dunno if you were able to test that or not, but its somehow the exact same problem as moment.

mihar-22 commented 4 years ago

Ah it might be, I didn't check it and I cleaned up the project. Does import * as dayjs from 'dayjs' do the trick or same issue as moment where you lose the ability to call a function?

therealpaulgg commented 4 years ago

same problem as moment, my whole project complains when i do any import like that

mihar-22 commented 4 years ago

That hack I linked earlier might do the trick then if moment and dayjs are tricky:

import * as dayjs from "dayjs";
const dayjs = require("dayjs").default || require("dayjs");
ngavinsir commented 3 years ago

Hey guys, is there any update on this? I'm also experiencing the same issue when I try to use the clsx module. The error is kinda similar: clsx is not a function. Have you @therealpaulgg solve your dayjs is not a function error?

therealpaulgg commented 3 years ago

@ngavinsir unfortunately I don't recall this ever being solved for me. I have not worked on my project that required svelte in a while. Not sure if it is indicative of a problem with svelte-jester or something else.

ngavinsir commented 3 years ago

Ah, okay, thanks for your response :+1:

miaoz2001 commented 3 years ago

Hi @mihar-22, I have the same issue as @therealpaulgg (except I use svelte-calendar, not svelte-flatpickr). following the instructions above, I got the svelte-calendar issue fixed by adding the resolver.

but, the solution about dayjs does not work: the trick const dayjs = require("dayjs").default || require("dayjs"); does fix the issue with jest, but the same code will cause a problem in browser with an error require is not defined, because require() does not exist in the browser/client-side JavaScript.

Do you have any workaround for dayjs? thanks

wgrabias commented 3 years ago

A nasty workaround for clsx:

  1. In your package.json:

    "jest": {
    ...
    "moduleNameMapper": {
      "^clsx$": [
        "<rootDir>/src/clsx.jest.js"
      ]
    }
    }
  2. Create a new file /src/clsx.jest.js

const clsx = require('clsx/dist/clsx.js')
module.exports.default = clsx
sebastianrothe commented 2 years ago

Hey guys, is there any update on this? I'm also experiencing the same issue when I try to use the clsx module. The error is kinda similar: clsx is not a function. Have you @therealpaulgg solve your dayjs is not a function error?

Can you please retry with the latest version and jest 27+?

bibizio commented 2 years ago

Hi! I had the same problem, the hack I came up with is:

In my test setup script ( setupFilesAfterEnv: ["./jestSetup.ts"] in jest.config.js)

jest.mock('moment', () => ({ _esModule: true, default: jest.requireActual('moment'), }));

Hope it helps

simeonTsonev commented 2 years ago

A nasty workaround for clsx:

  1. In your package.json:
  "jest": {
    ...
    "moduleNameMapper": {
      "^clsx$": [
        "<rootDir>/src/clsx.jest.js"
      ]
    }
  }
  1. Create a new file /src/clsx.jest.js
const clsx = require('clsx/dist/clsx.js')
module.exports.default = clsx

That's the only solution which works for me. I use it for my troubles with Flatpickr: In your package.json:

  moduleNameMapper: {
    "^flatpickr$": ["<rootDir>/src/flatpickr.jest.js"]
    }

and then you make a new file ''/src/flatpickr.jest.js:

const flatpickr = require('flatpickr/dist/flatpickr.js')
module.exports.default = flatpickr

Thank you @wgrabias

illright commented 2 years ago

It seems like TypeScript isn't the real culprit here. @sebastianrothe I've created a reproduction repository with the latest versions of Jest and svelte-jester to demonstrate that the issue persists.

What's confusing about this issue is that if one uses svelte-add-jest to add Jest to a SvelteKit application, such issues won't come up because this adder generates code that runs Jest in experimental ESM mode. It's all good, but then the user-event part of the Testing Library doesn't work, so it's a no-go zone for proper testing.

sebastianrothe commented 2 years ago

We have some tests with click-events, also. What is not working on your side?

illright commented 2 years ago

@sebastianrothe never mind, turns out it's an issue with @testing-library/user-event (https://github.com/testing-library/user-event/issues/757#issuecomment-1009837483). I've created a reproduction of it in the esm branch of the same repository in case you'd like to take a look.

However, I would personally prefer not using the experimental ES module support. Is there any chance this could be fixed inside of svelte-jester to enable both modes?

sebastianrothe commented 2 years ago

We do ship both versions (CJS and ESM). You may have to manually override the transformer in your jest.config:

transformer: "svelte-jester/dist/transformer.cjs",
illright commented 2 years ago

You may have to manually override the transformer in your jest.config:

This configuration didn't resolve the issue in the reproduction repository:

export default {
  testEnvironment: "jsdom",
  transform: {
    "^.+\\.js$": "babel-jest",
    "^.+\\.svelte$": "svelte-jester/dist/transformer.cjs"
  },
  moduleFileExtensions: ["js", "svelte"]
};

Did I understand you correctly?

sebastianrothe commented 2 years ago

Yes, thats correct. But, I saw that your project is ESM (https://github.com/illright/jest-clsx-repro/blob/main/package.json#L21).

Jest is the culprit, sorry. Jest needs the experimental flag to work in ESM mode(https://jestjs.io/docs/ecmascript-modules). Node understands ESM natively.