Skip to content

Instantly share code, notes, and snippets.

@danpetitt
Last active November 27, 2024 15:18

Revisions

  1. danpetitt revised this gist Feb 23, 2023. 1 changed file with 22 additions and 2 deletions.
    24 changes: 22 additions & 2 deletions esmodules.md
    Original file line number Diff line number Diff line change
    @@ -47,8 +47,22 @@ Use `esnext` module type; this is my basic tsconfig for building:
    There is a fundamental difference to how you name and import your files when using ESM.

    What you do will depend if you want to use:
    * mostly esm, but have some common js files: leave `.ts` and rename commonjs files as `.cjs`
    * mostly use common js, but have some typescript esm files: use `.mts` and leave commonjs files as `.js`
    * mostly esm, but have some common js files
    * mostly use commonjs, but have some esm files

    Basically, I leave all my files as `.ts` and will use them all as ESM; and the handful of files that need to be treated as CommonJS will be named `.cjs` (or `.mts`).

    The following from the Typescript Manual helps give more information determination of this.

    > The type field in package.json is nice because it allows us to continue using the .ts and .js file extensions which can be convenient; however, you will occasionally need to write a file that differs from what type specifies. You might also just prefer to always be explicit.
    > Node.js supports two extensions to help with this: .mjs and .cjs. .mjs files are always ES modules, and .cjs files are always CommonJS modules, and there’s no way to override these.
    > In turn, TypeScript supports two new source file extensions: .mts and .cts. When TypeScript emits these to JavaScript files, it will emit them to .mjs and .cjs respectively.
    > Furthermore, TypeScript also supports two new declaration file extensions: .d.mts and .d.cts. When TypeScript generates declaration files for .mts and .cts, their corresponding extensions will be .d.mts and .d.cts.
    > Using these extensions is entirely optional, but will often be useful even if you choose not to use them as part of your primary workflow.
    Once this has been determined, the last thing you need to do is change your imports to reference the `js` built files:

    @@ -98,3 +112,9 @@ One last thing is we now need to import the jest global variable by adding to th
    ```typescript
    import { jest } from '@jest/globals';
    ```

    ## References
    * [Typescript ECMAScript Module Support](https://www.typescriptlang.org/docs/handbook/esm-node.html)
    * [Jest ECMAScript Modules](https://jestjs.io/docs/ecmascript-modules)
    * [TS Jest ECMAScript Module Support](https://kulshekhar.github.io/ts-jest/docs/guides/esm-support)
    * [NodeJS ECMAScript Modules](https://nodejs.org/api/esm.html)
  2. danpetitt created this gist Feb 23, 2023.
    100 changes: 100 additions & 0 deletions esmodules.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,100 @@
    # Typescript, Jest and ECMAScript Modules (ESM)

    ## Package.json

    Add the `type` property to `package.json` to ensure modules are supported:

    ```json
    {
    "type": "module",
    }
    ```

    Ensure you are using latest `jest` and `ts-jest`; I have been testing using the following versions:
    * "ts-jest": "^29.0.5"
    * "jest": "^29.4.3"

    ## tsconfig

    Use `esnext` module type; this is my basic tsconfig for building:

    ```json
    {
    "extends": "@tsconfig/node18/tsconfig.json",
    "compilerOptions": {
    "module": "esnext",
    "resolveJsonModule": true,
    "removeComments": true,
    "outDir": "../dist",
    "sourceMap": false,
    "types": ["node", "jest"],
    "strict": false,
    "esModuleInterop": true
    },
    "include": [
    "./**/*",
    "./**/*.json"
    ],
    "exclude": [
    "tsconfig.json",
    "tsconfig-build.json"
    ]
    }
    ```

    ## Filenames and Imports

    There is a fundamental difference to how you name and import your files when using ESM.

    What you do will depend if you want to use:
    * mostly esm, but have some common js files: leave `.ts` and rename commonjs files as `.cjs`
    * mostly use common js, but have some typescript esm files: use `.mts` and leave commonjs files as `.js`

    Once this has been determined, the last thing you need to do is change your imports to reference the `js` built files:

    ```typescript
    import { export } from './source.file.js'
    ```

    This can be annoying, but once you have changed your first import, most editors (I use VSCode) will auto add any further imports
    with the extension included. However it is something you need to keep a track of when you create a new file and add the first import.

    Any files which have to be treated as CommonJS files should be renamed `.cjs`; common files will be `eslintrc.cjs`, or any integration startup scripts like `pre-test-integration.cjs`.

    ## Jest

    Change your test start instruction in `package.json` to set the `experimental-vm-modules` support which is required as at `v18.8.0` of node:

    ```json
    "scripts": {
    "pretest": "npm run lint && tsc -p tsconfig.json --noEmit",
    "test": "NODE_OPTIONS=--experimental-vm-modules jest --verbose --coverage --config jest-unit.config.json"
    }
    ```

    ### Transformation

    As we are using `ts-jest` as our transformer; the setup config, either as a `module.export` or via a `.json` file (as shown below), needs some additional properties and changes to the structure of the setup properties.

    As you can see, we need to include an additional `moduleNameMapper` so that imports work; a new `extensionsToTreatAsEsm` property and to change the structure of `transform` to include the new `.cjs` files but also allow us to tell ts-jest to emit ESModule compatible code.

    ```json
    "moduleNameMapper": {
    "^(\\.{1,2}/.*)\\.js$": "$1",
    },
    "extensionsToTreatAsEsm": [".ts"],
    "transform": {
    "^.+\\.(mt|t|cj|j)s$": [
    "ts-jest",
    {
    "useESM": true
    }
    ]
    },
    ```

    One last thing is we now need to import the jest global variable by adding to the top of our test files:

    ```typescript
    import { jest } from '@jest/globals';
    ```