Jest 不会接受 NodeJS16 和 TypeScript 的顶级等待

Jest won't accept top-level-awaits with NodeJS16 & TypeScript

我正在尝试将我的 NodeJS12 和 TypeScript 应用程序更新到 Node16,如果原因是需要使用顶级等待。

更新后代码编译正确,但 Jest 不接受特定的顶级等待代码:

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 } },
};

我真的不明白这里出了什么问题。有什么想法吗?

最近突然想到,据我所知,CJS 不支持top-level await,这意味着你需要使用ts-jest 和esm。

我的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/"
    },
    "collectCoverage": true,
    "coverageDirectory": "./coverage",
    "collectCoverageFrom": ["src/**/*.(t|j)s"],
    "coveragePathIgnorePatterns": [
        ".module.ts$",
        ".spec.ts$",
        "src/database/",
        "src/server.ts"
    ],
    "verbose": true
}

您还需要 运行 使用 --experimental-vm-modules 选项开玩笑

node --experimental-vm-modules -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand --config jest.overall.json

经过一些研究,我可以肯定地告诉你,我们离可靠的解决方案还很远。主要问题是 top-level await 可用

when the 'module' option is set to 'es2022', 'esnext', 'system', or 'nodenext', and the 'target' option is set to 'es2017' or higher.

即使你在tsconfig.json中将moduletarget都设置为es2022,你也必须理解和解决很多错误,因为你有有经验的。我刚刚找到适合我的配置现在,但它可能会出现我不知道的其他问题。


$ node --version
v17.7.2
$ npm list
myproject@1.0.0 /home/me/folder
├── @types/jest@27.4.1
├── @types/node@17.0.25
├── config@3.3.7
├── jest@27.5.1
├── sequelize@6.18.0
├── timespan@2.3.0
├── ts-jest@27.1.4
├── ts-node@10.7.0
├── ...
└── typescript@4.6.3

我还有其他库,但不需要重新改编。我使用 jest 进行测试,sequelize 作为 ORM:它们需要适当配置。让我们从 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"说明here
  • scripts 部分中的 start 命令需要选项 --es-module-specifier-resolution=node,这是启用 ES 模块而不是默认的 CommonJS 模块所必需的。
  • (仅适用于 Jest)--no-warnings 不是强制性的,但 --experimental-vm-modules 是。

至于 Jest 配置:

$ 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;

当然,您可以添加新属性,但这或多或少是最低配置。您可以找到其他预设 here (in case you need them), while the globals section for ts-jest is furtherly explained here.


让我们看看tsconfig.json,它有点复杂:

$ 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"]
}

同样,它或多或少是最小配置,我想 moduletarget 属性可以用尝试使用 [=136= 时显示的错误中的其他选项初始化] await,但我不确定它们是否都在工作。


现在最糟糕的是包与 ES 模块的兼容性。我没有研究足够告诉你什么时候来自某个包的 import 需要调整,但我会为你提供一些例子。

Built-in 图书馆/config/timespan

Built-in 图书馆应该没问题。在我的项目中,我还使用了 configtimespan:

  • timespanimport * as timespan from "timespan"。例如,可以使用 new timespan.Timespan(...) 访问 Timespan 对象。
  • config: import config from 'config'.
  • util:使用 built-in 包,我认为您可以像使用 CommonJS 语法一样导入任何导出的函数。例如,import { format } from 'util'.

sequelize(以及许多其他...)

我在打字稿中使用 sequelize,但我不得不更改 import 语法。我遇到的错误是:

SyntaxError: The requested module 'sequelize' does not provide an export named 'DataTypes' at ...

问题是experimental modules do not support named exports。新的 import 语法取决于您需要导入的内容:

  • 对于类型,您仍然可以像以前一样导入它们,例如 import { Type1, Type2 } from 'your_library'
  • 对于类(但我认为一切都来自值space),你首先需要导入一个默认对象,然后使用对象解构来提取需要的值。

例如 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>
    ...
}

其他来源