Anyone gotten unit tests working with ES6 modules?

I’m trying to convert my Rescript React project from CreateReactApp to Vite using native ES6 modules. I have everything working, except for tests which currently use bs-jest. I’ve tried every variation of Jest + ES6, Jest + Babel + ES6 trying to get these to compile with no luck. I tried the new retest lib, with the same issues:

SyntaxError: Cannot use import statement outside a module

Does anyone have a working test setup and "module": "es6", in your bsconfig?

1 Like

This is the configuration I’m using for ES6 and Jest:

bsconfig.json

{
  "name": "demo",
  "version": "0.0.1",
  "sources": [
    {
      "dir": "src",
      "subdirs": true
    },
    {
      "dir": "__tests__",
      "type": "dev"
    }
  ],
  "package-specs": {
    "module": "es6",
    "in-source": true,
    "suffix": ".mjs"
  },
  "namespace": true,
  "bs-dependencies": [],
  "bs-dev-dependencies": [
    "@glennsl/bs-jest"
  ],
  "warnings": {
    "error": "+101"
  }
}
jest.config.js

module.exports = {
  testMatch: ["**/__tests__/*.?(m)js", "**/?(*.)+_test?(.bs).js?(x)"],
  moduleFileExtensions: ["js", "jsx", "mjs"]
};
package.json (I'm using pnpm instead of npm, hence the pnpx call in the scripts section for the tests)

{
  "name": "demo",
  "version": "0.0.1",
  "scripts": {
    "build": "bsb -make-world",
    "clean": "bsb -clean-world",
    "start": "bsb -make-world -w",
    "test": "NODE_OPTIONS=--experimental-vm-modules pnpx jest",
    "test:watch": "NODE_OPTIONS=--experimental-vm-modules pnpx jest --watch"
  },
  "keywords": [
    "rescript"
  ],
  "author": "",
  "license": "MIT",
  "devDependencies": {
    "@glennsl/bs-jest": "^0.7.0",
    "bs-platform": "*"
  }
}

With Rescript 9.02 I was just having too much trouble trying to get Babel to work and since I really didn’t need Babel for what I what I was doing using the .mjs extension and the Node.js options in my package.json worked best for me. If you need Babel since you’re doing React you might have to drop back to an older version of ReScript and use the .bs.js extension.

I’ve only done a basic React set up with Vite and it takes well to the .mjs extension but I haven’t tried it out with any other 3rd party libraries yet. These are my configs for React with Vite (I found an old ReasonML Vite set up on Github and made changes for it to work with ReScript):

bsconfig.json

{
  "name": "rescript-react-vite-starter",
  "reason": {
    "react-jsx": 3
  },
  "sources": {
    "dir": "src",
    "subdirs": true
  },
  "bsc-flags": [
    "-bs-super-errors",
    "-bs-no-version-header"
  ],
  "package-specs": [
    {
      "module": "es6",
      "in-source": true,
      "suffix": ".mjs"
    }
  ],
  "namespace": true,
  "bs-dependencies": [
    "@rescript/react"
  ]
}
package.json

 "name": "rescript-react-vite",
  "version": "0.0.0",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "serve": "vite preview",
    "res:build": "bsb -make-world -clean-world",
    "res:watch": "bsb -make-world -clean-world -w",
    "res:clean": "bsb -clean-world"
  },
  "keywords": [
    "rescript",
    "rescript-react",
    "vite"
  ],
  "dependencies": {
    "react": "^17.0.0",
    "react-dom": "^17.0.0"
  },
  "devDependencies": {
    "@rescript/react": "^0.10.1",
    "@vitejs/plugin-react-refresh": "^1.1.0",
    "bs-platform": "9.0.2",
    "vite": "^2.0.1"
  }
}
vite.config.js
import { defineConfig } from 'vite'
import reactRefresh from '@vitejs/plugin-react-refresh'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [reactRefresh()]
})

Again, these are using the latest version of ReScript. If you’re using Vite I’m guessing you shouldn’t need Babel because Vite uses ESBuild. But if you really need Babel there are solutions in the forum that have Jest+Babel set ups but you’d likely need to use one of the older versions of ReScript because of the changes made to get ReScript to work with the .mjs extension was breaking stuff that used non-ES6 libraries.

I couldn’t get everything working using the latest ReScript and Babel so the .mjs extension solution worked for me with Jest and Vite seems like it will take care of my frontend ES6 needs without the headache of trying to get Babel working again or going back to an older version of ReScript.

2 Likes

Try https://gist.github.com/alexfedoseev/d6a37187fd9a673e4b2fe21f8f2f2e0e

That’s what I am using in my package.json for jest.

  "jest": {
    "preset": "react-native",
    "testRegex": "Test\\.js$",
    "transformIgnorePatterns": [
      "node_modules/(?!(jest-)?react-native|react-(native|universal|navigation)-(.*)|@react-native-community/(.*)|@react-navigation/(.*)|bs-platform|(@[a-zA-Z]+/)?(bs|reason|rescript)-(.*)+)"
    ]
  },

The transformIgnorePatterns is flexible enough to not have to be updated often :slight_smile:

Source: https://github.com/MoOx/LifeTime/blob/e344af5144eb30f2243a0c9515f198243d665c78/package.json#L101

Hrm, I tried this and hit the same issue. I’m starting to wonder if it’s my version of Node (15.2.1):

 FAIL  src/__tests__/Route_test.bs.js
  ● Test suite failed to run

    Jest encountered an unexpected token

    This usually means that you are trying to import a file which Jest cannot parse, e.g. it's not plain JavaScript.

    By default, if Jest sees a Babel config, it will use that to transform your files, ignoring "node_modules".

    Here's what you can do:
     • If you are trying to use ECMAScript Modules, see https://jestjs.io/docs/en/ecmascript-modules for how to enable it.
     • To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in your config.
     • If you need a custom transformation specify a "transform" option in your config.
     • If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the "moduleNameMapper" config option.

    You'll find more details and examples of these config options in the docs:
    https://jestjs.io/docs/en/configuration.html

    Details:

    /Users/dfalling/Code/ido/node_modules/@glennsl/bs-json/src/Json_decode.bs.js:3
    import * as List from "bs-platform/lib/es6/list.js";
    ^^^^^^

    SyntaxError: Cannot use import statement outside a module

      3 | import * as Curry from "bs-platform/lib/es6/curry.js";
      4 | import * as Belt_Result from "bs-platform/lib/es6/belt_Result.js";
    > 5 | import * as Json_decode from "@glennsl/bs-json/src/Json_decode.bs.js";
        | ^
      6 | import * as Caml_js_exceptions from "bs-platform/lib/es6/caml_js_exceptions.js";
      7 |
      8 | function decodeHelper(decode, json) {

      at Runtime.createScriptFromCode (node_modules/jest-runtime/build/index.js:1350:14)
      at Object.<anonymous> (src/data/Decoders.bs.js:5:1)

Oh wow, this got me the closest so far. I had to change my jest.config.js to use export default { since module.exports = is commonJS. Then all of my tests passed except one which needs an environment variable, so this definitely got me past the module issue.

Not in love with having to rename all my files to .mjs, but it is nice that I don’t have to pull Babel into my builds just for a few unit tests.

Thanks!

This is great topic.
I met same problem and found another way to solve.

  1. bsconfig.json(excerpt)
...

"sources": [
    {
        "dir": "test/rescript",
        "subdirs": true,
        "type": "dev"
    },
    {
        "dir": "src/rescript",
        "subdirs": true
    }
],

...

"package-specs": {
    "module": "es6",
    "in-source": true
},
"suffix": ".bs.js",
...
  1. jest.config.json
module.exports = {
  testMatch: ["**/test/rescript/**/*.?(m)js"],
  moduleFileExtensions: ["js", "jsx", "mjs"]
};
  1. required libraries
npm install --save-dev @babel/core @babel/preset-env @babel/plugin-transform-modules-commonjs @glennsl/bs-jest jest babel-jest replace-in-file
  1. create original transform script

es_to_cjs_for_jest.js

const replace = require('replace-in-file');
const babel = require("@babel/core");
const fs = require("fs")
const bsJest = "node_modules/@glennsl/bs-jest/src/jest.bs.js"
const testDir = "test/rescript"

function esToCjs(fileName) {
    let code = fs.readFileSync(fileName, 'utf-8');
    const babelObj = babel.transform(code, {
        plugins: ["@babel/plugin-transform-modules-commonjs"]
    });
    return babelObj.code;
}

function overwrite(fileName, code) {
    fs.writeFileSync(fileName, code);
}

function listFiles(dir) {
    let files = fs.readdirSync(dir, { withFileTypes: true }).flatMap(dirent => {
        const name = dirent.name;

        if (dirent.isFile()) {
            if (name.endsWith("bs.js")) {
                return [`${dir}/${name}`];
            } else {
                return [];
            }
        } else {
            return listFiles(`${dir}/${name}`);
        }
    });
    return files;
}

// Transform all tests of ReScript and bs-jest from ES6 to CommonJS.
const testFiles = [bsJest, ...listFiles(testDir)]
testFiles.forEach(fileName => {
    const code = esToCjs(fileName);
    overwrite(fileName, code);
})

// bs-jest uses es6 libraries of bs-platform
// these must be replaced to "js" (/lib/js/ is for commonjs)
try {
    const options = {
        files: bsJest,
        from: /bs-platform\/lib\/es6*/g,
        to: 'bs-platform/lib/js',
    };
    const results = replace.sync(options);
}
catch (error) {
    console.error('Error occurred:', error);
}
  1. execute tests
npx bsb -make-world && node es_to_cjs_for_jest.js && npx jest

That is all.
I wrote more detailed about this on my blog.(But japanese…sorry)

Of course the best answer method that is on this topic is the smartest.

But i want to use genType library.
genType requires that file name ends with “bs.js”.

My way is really ugly and a little slowly to execute test because everytime convert ES6 to CommonJS.
But with it existing ES6 project of ReScript have to not to change.

I try to use ReScript for Svelte.
And for that i got this solution.
I will share how to use ReScript with Svelte on this forum in the near future.

Wow, yeah I think a solution like this is what I was needing- I got really close several different ways but hit the same problems you specifically called out. Like dependencies still being in ES6.