'Jest won't accept top-level-awaits with NodeJS16 & TypeScript
I'm trying to update my NodeJS12 & TypeScript app to Node16, one if the reasons is the need to use top-level-awaits.
The code compiles correctly after the update, but Jest won't accept the specific top-level-await code:
ts-jest[ts-compiler] (WARN) src/xxx.ts:11:17 - error TS1378: Top-level 'await' expressions are only allowed when the 'module' option is set to 'es2022', 'esnext', 'system', or 'nodenext', and the 'target' option is set to 'es2017' or higher.
11 const project = await client.getProjectId();
~~~~~
FAIL src/xxx.test.ts
● Test suite failed to run
Jest encountered an unexpected token
package.json:
{
"name": "functions",
"scripts": {
"lint": "eslint --ext .js,.ts .",
"lint:fix": "eslint --ext .js,.ts . --fix",
"build": "tsc -b",
"build:watch": "tsc-watch",
"serve": "...",
"test": "env-cmd -f .env.json jest --runInBand --verbose"
},
"type": "module",
"engines": {
"node": "16"
},
"main": "lib/index.js",
"exports": "./lib/index.js",
"dependencies": {
"test": "^1.0.0"
},
"devDependencies": {
"@google-cloud/functions-framework": "^2.1.0",
"@types/busboy": "^1.3.0",
"@types/compression": "1.7.2",
"@types/cors": "^2.8.12",
"@types/express": "^4.17.12",
"@types/express-serve-static-core": "^4.17.28",
"@types/google-libphonenumber": "^7.4.23",
"@types/jest": "^27.4.0",
"@types/jsonwebtoken": "^8.5.7",
"@types/luxon": "^2.0.9",
"@types/node-zendesk": "^2.0.6",
"@types/sinon": "^10.0.6",
"@types/supertest": "^2.0.11",
"@types/swagger-ui-express": "^4.1.3",
"@types/uuid": "^8.3.4",
"@types/yamljs": "^0.2.31",
"@typescript-eslint/eslint-plugin": "^5.9.0",
"@typescript-eslint/parser": "^5.9.0",
"env-cmd": "^10.1.0",
"eslint": "^8.6.0",
"eslint-config-google": "^0.14.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react-hooks": "^4.2.1-beta-a65ceef37-20211130",
"jest": "^27.4.7",
"prettier": "^2.5.1",
"sinon": "^12.0.1",
"supertest": "^6.2.1",
"ts-jest": "^27.1.3",
"ts-node": "^10.4.0",
"tsc-watch": "^4.6.0",
"typescript": "^4.5.4"
},
"private": true
}
tsconfig.json:
{
"compilerOptions": {
"module": "es2022",
"noImplicitReturns": true,
"noUnusedLocals": true,
"outDir": "lib",
"sourceMap": false,
"strict": true,
"target": "es2021",
"moduleResolution": "Node",
"resolveJsonModule": true
},
"compileOnSave": true,
"include": [
"src"
],
"ts-node": {
"moduleTypes": {
"jest.config.ts": "cjs"
}
}
}
jest.config.ts:
export default {
roots: [
'<rootDir>/src'
],
setupFiles: ['./src/setupJestEnv.ts'],
preset: 'ts-jest',
testEnvironment: 'node',
testPathIgnorePatterns: ['/node_modules/'],
coverageDirectory: './coverage',
coveragePathIgnorePatterns: ['node_modules', 'src/database', 'src/test', 'src/types'],
globals: { 'ts-jest': { diagnostics: false } },
};
I can't really understand what's wrong here. Any idea?
Solution 1:[1]
After some research, I can certainly tell you that we are still far from a reliable solution. The main problem is that the top-level await
is available
when the 'module' option is set to 'es2022', 'esnext', 'system', or 'nodenext', and the 'target' option is set to 'es2017' or higher.
Even if you set both module
and target
to es2022
in the tsconfig.json
, you will have to understand and solve a lot of errors, as you have experienced. I have just found a configuration that works for me now, but it might present other problems I am not aware of.
$ node --version
v17.7.2
$ npm list
[email protected] /home/me/folder
??? @types/[email protected]
??? @types/[email protected]
??? [email protected]
??? [email protected]
??? [email protected]
??? [email protected]
??? [email protected]
??? [email protected]
??? ...
??? [email protected]
I have other libraries, but they did not have to be readapted. I am using jest
for testing and sequelize
as ORM: they need to be configured appropriately. Let's start from the package.json
:
{
"type": "module",
"scripts": {
"prestart": "npx tsc",
"start": "NODE_ENV=production node --es-module-specifier-resolution=node out/index.js",
"test": "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js tests/"
},
...
}
"type": "module"
is explained here- The
start
command in thescripts
section needs the option--es-module-specifier-resolution=node
, necessary to enable ES Modules instead of the default CommonJS modules. - (For Jest only):
--no-warnings
is not mandatory, but--experimental-vm-modules
is.
As for the Jest configurations:
$ cat jest.config.ts
import type { Config } from '@jest/types';
const config: Config.InitialOptions = {
globals: {
"ts-jest": {
useESM: true
}
},
preset: 'ts-jest/presets/default-esm',
roots: ["tests/"],
modulePathIgnorePatterns: [
"<rootDir>/node_modules/"
]
};
export default config;
You can add new properties, of course, but this is more or less the minimal configuration. You can find the other presets here (in case you need them), while the globals
section for ts-jest
is furtherly explained here.
Let's see the tsconfig.json
, which is a bit more complex:
$ cat tsconfig.json
{
"ts-node": {
"moduleTypes": {
"jest.config.ts": "cjs"
}
},
"compilerOptions": {
"lib": ["ES2021"],
"module": "ESNext",
"esModuleInterop": true,
"moduleResolution": "node",
"target": "ESNext",
"outDir": "out",
"resolveJsonModule": true
},
"compileOnSave": true,
"include": ["src/"],
"exclude": ["node_modules", "**/*.spec.ts"]
}
Again, it is more or less the minimal configuration, I guess module
and target
properties can be initialized with the other alternatives in the error shown when trying to use the top-level await
, but I am not sure they are all working.
Now to the worst part, the packages' compatibility with ES modules. I have not studied enough to tell you when an import
from a certain package needs to be adapted, but I will provide you with some examples.
Built-in libraries/config
/timespan
Built-in libraries should be ok as they are. In my project I am also using config
and timespan
:
timespan
:import * as timespan from "timespan"
. TheTimespan
object is accessible usingnew timespan.Timespan(...)
for example.config
:import config from 'config'
.util
: with built-in packages, I think you can import any exported function as if you are using the CommonJS syntax. For instance,import { format } from 'util'
.
sequelize
(and many others...)
I am using sequelize
with typescript, but I had to change the import
syntax. The error I have encountered is:
SyntaxError: The requested module 'sequelize' does not provide an export named 'DataTypes' at ...
The problem is that experimental modules do not support named exports. The new import
syntax depends on what you need to import:
- for types, you can still import them as before, like
import { Type1, Type2 } from 'your_library'
- for classes (but I think everything that comes from the value space), you first need to import a default object, and then use the object destructuring to extract the needed values.
For example, with sequelize
:
import Sequelize,
{ InferAttributes, InferCreationAttributes, CreationOptional }
from 'sequelize'; // InferAttributes, InferCreationAttributes, CreationOptional are types
const { Op, DataTypes, Model } = Sequelize // these are classes
class UserModel extends Model<InferAttributes<UserModel>,
InferCreationAttributes<UserModel>>
{
declare id: CreationOptional<number>
...
}
Additional sources
Solution 2:[2]
It occurred to me lately, as far as I know, CJS doesn't support top-level await, meaning that you need to use ts-jest with esm.
my jest.config.json
{
"extensionsToTreatAsEsm": [".ts"],
"globals": {
"ts-jest": {
"useESM": true
}
},
"preset": "ts-jest/presets/default-esm",
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": "spec.ts$",
"modulePathIgnorePatterns": ["<rootDir>/dist/", "<rootDir>/node_modules/"],
"moduleNameMapper": {
"^src/(.*)": "<rootDir>/src/$1"
},
"collectCoverage": true,
"coverageDirectory": "./coverage",
"collectCoverageFrom": ["src/**/*.(t|j)s"],
"coveragePathIgnorePatterns": [
".module.ts$",
".spec.ts$",
"src/database/",
"src/server.ts"
],
"verbose": true
}
You will also need to run jest with --experimental-vm-modules option
node --experimental-vm-modules -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand --config jest.overall.json
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 | Null Bite |