Skip to content

SWC

SWC(Speedy Web Compiler)是一个基于 Rust 的可扩展平台,可用于编译与打包。 在 Nest CLI 中使用 SWC,是一种极佳且简单的方式,可以显著加速你的开发过程。

提示

SWC 大约比默认的 TypeScript 编译器快 20 倍

安装

首先安装几个包:

bash
$ npm i --save-dev @swc/cli @swc/core

入门

安装完成后,你就可以在 Nest CLI 中使用 swc builder:

bash
$ nest start -b swc
# OR nest start --builder swc

提示

如果你的仓库是 monorepo,请查看这一节

除了传 -b 标志之外,你也可以直接在 nest-cli.json 中将 compilerOptions.builder 属性设为 "swc",如下所示:

json
{
  "compilerOptions": {
    "builder": "swc"
  }
}

如果你想自定义 builder 行为,可以传入一个包含 type(值为 "swc")和 options 两个属性的对象,例如:

json
{
  "compilerOptions": {
    "builder": {
      "type": "swc",
      "options": {
        "swcrcPath": "infrastructure/.swcrc",
      }
    }
  }
}

例如,若要让 swc 编译 .jsx.tsx 文件,可以这样配置:

json
{
  "compilerOptions": {
    "builder": {
      "type": "swc",
      "options": { "extensions": [".ts", ".tsx", ".js", ".jsx"] }
    },
  }
}

要以 watch 模式运行应用,请使用以下命令:

bash
$ nest start -b swc -w
# OR nest start --builder swc --watch

类型检查

SWC 本身不会执行类型检查(这点与默认的 TypeScript 编译器不同),因此如果要启用类型检查,你需要使用 --type-check 标志:

bash
$ nest start -b swc --type-check

该命令会让 Nest CLI 在 SWC 旁边额外以 noEmit 模式运行 tsc,从而异步完成类型检查。同样地,除了显式传 --type-check 标志外,你也可以直接在 nest-cli.json 中把 compilerOptions.typeCheck 设为 true,如下所示:

json
{
  "compilerOptions": {
    "builder": "swc",
    "typeCheck": true
  }
}

CLI 插件(SWC)

--type-check 标志还会自动执行 NestJS CLI 插件,并生成一个序列化后的元数据文件,随后应用可以在运行时加载它。

SWC 配置

SWC builder 已预先配置好以满足 NestJS 应用的需求。不过,你仍然可以在项目根目录创建 .swcrc 文件,并按需要调整配置项。

json
{
  "$schema": "https://swc.rs/schema.json",
  "sourceMaps": true,
  "jsc": {
    "parser": {
      "syntax": "typescript",
      "decorators": true,
      "dynamicImport": true
    },
    "baseUrl": "./"
  },
  "minify": false
}

Monorepo

如果你的仓库是 monorepo,那么你不能直接使用 swc builder,而是需要配置 webpack 来使用 swc-loader

首先,安装所需包:

bash
$ npm i --save-dev swc-loader

安装完成后,在应用根目录创建一个 webpack.config.js 文件,并填入以下内容:

js
const swcDefaultConfig = require('@nestjs/cli/lib/compiler/defaults/swc-defaults').swcDefaultsFactory().swcOptions;

module.exports = {
  module: {
    rules: [
      {
        test: /\.ts$/,
        exclude: /node_modules/,
        use: {
          loader: 'swc-loader',
          options: swcDefaultConfig,
        },
      },
    ],
  },
};

Monorepo 与 CLI 插件

如果你还使用了 CLI 插件,那么 swc-loader 不会自动加载它们。相反,你需要创建一个单独文件,手动加载这些插件。为此,请在 main.ts 旁边创建一个 generate-metadata.ts 文件,内容如下:

ts
import { PluginMetadataGenerator } from '@nestjs/cli/lib/compiler/plugins/plugin-metadata-generator';
import { ReadonlyVisitor } from '@nestjs/swagger/dist/plugin';

const generator = new PluginMetadataGenerator();
generator.generate({
  visitors: [new ReadonlyVisitor({ introspectComments: true, pathToSource: __dirname })],
  outputDir: __dirname,
  watch: true,
  tsconfigPath: 'apps/<name>/tsconfig.app.json',
});

提示

这个示例中我们用了 @nestjs/swagger 插件,但你也可以替换成任意你需要的插件。

generate() 方法接受以下选项:

watch是否监听项目变化。
tsconfigPathtsconfig.json 文件路径。相对于当前工作目录(process.cwd())。
outputDir元数据文件保存目录路径。
visitors用于生成元数据的 visitor 数组。
filename元数据文件名。默认为 metadata.ts
printDiagnostics是否将诊断信息输出到控制台。默认为 true

最后,你可以在另一个终端窗口中运行 generate-metadata 脚本:

bash
$ npx ts-node src/generate-metadata.ts
# OR npx ts-node apps/{YOUR_APP}/src/generate-metadata.ts

常见陷阱

如果你的应用使用了 TypeORM/MikroORM 或任何其他 ORM,你可能会遇到循环导入问题。SWC 对循环导入处理得并不好,因此你应使用如下变通方案:

typescript
@Entity()
export class User {
  @OneToOne(() => Profile, (profile) => profile.user)
  profile: Relation<Profile>; // <--- see "Relation<>" type here instead of just "Profile"
}

提示

Relation 类型由 typeorm 包导出。

这样做可以避免属性类型被保存进转译后的属性元数据中,从而规避循环依赖问题。

如果你的 ORM 没有提供类似方案,你也可以自己定义一个包装类型:

typescript
/**
 * Wrapper type used to circumvent ESM modules circular dependency issue
 * caused by reflection metadata saving the type of the property.
 */
export type WrapperType<T> = T; // WrapperType === Relation

对于项目中的所有循环依赖注入,你同样需要使用上面提到的自定义包装类型:

typescript
@Injectable()
export class UsersService {
  constructor(
    @Inject(forwardRef(() => ProfileService))
    private readonly profileService: WrapperType<ProfileService>,
  ) {};
}

Jest + SWC

要在 Jest 中使用 SWC,需要安装以下包:

bash
$ npm i --save-dev jest @swc/core @swc/jest

安装完成后,根据你的配置方式,更新 package.jsonjest.config.js 文件,加入以下内容:

json
{
  "jest": {
    "transform": {
      "^.+\\.(t|j)s?$": ["@swc/jest"]
    }
  }
}

此外,你还需要在 .swcrc 文件中加入以下 transform 属性:legacyDecoratordecoratorMetadata

json
{
  "$schema": "https://swc.rs/schema.json",
  "sourceMaps": true,
  "jsc": {
    "parser": {
      "syntax": "typescript",
      "decorators": true,
      "dynamicImport": true
    },
    "transform": {
      "legacyDecorator": true,
      "decoratorMetadata": true
    },
    "baseUrl": "./"
  },
  "minify": false
}

如果你的项目里使用了 NestJS CLI 插件,那么你需要手动运行 PluginMetadataGenerator。详情请查看这一节

Vitest

Vitest 是一个为 Vite 设计的快速、轻量级测试运行器。它提供了一种现代、快速且易用的测试方案,并且能够集成到 NestJS 项目中。

安装

首先安装所需包:

bash
$ npm i --save-dev vitest unplugin-swc @swc/core @vitest/coverage-v8

配置

在应用根目录中创建一个 vitest.config.ts 文件,内容如下:

ts
import swc from 'unplugin-swc';
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globals: true,
    root: './',
  },
  plugins: [
    // This is required to build the test files with SWC
    swc.vite({
      // Explicitly set the module type to avoid inheriting this value from a `.swcrc` config file
      module: { type: 'es6' },
    }),
  ],
  resolve: {
    alias: {
      // Ensure Vitest correctly resolves TypeScript path aliases
      'src': resolve(__dirname, './src'),
    },
  },
});

这个配置文件会设置 Vitest 运行环境、根目录以及 SWC 插件。你还应该为 e2e 测试创建一个单独配置文件,并额外添加一个 include 字段来指定测试路径的正则匹配:

ts
import swc from 'unplugin-swc';
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    include: ['**/*.e2e-spec.ts'],
    globals: true,
    root: './',
  },
  plugins: [swc.vite()],
});

另外,你还可以设置 alias 选项,以在测试中支持 TypeScript 路径别名:

ts
import swc from 'unplugin-swc';
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    include: ['**/*.e2e-spec.ts'],
    globals: true,
    alias: {
      '@src': './src',
      '@test': './test',
    },
    root: './',
  },
  resolve: {
    alias: {
      '@src': './src',
      '@test': './test',
    },
  },
  plugins: [swc.vite()],
});

路径别名

与 Jest 不同,Vitest 不会自动解析 TypeScript 路径别名(如 src/)。这可能导致测试期间出现依赖解析错误。要解决该问题,请在 vitest.config.ts 中加入如下 resolve.alias 配置:

ts
import { resolve } from 'path';

export default defineConfig({
  resolve: {
    alias: {
      'src': resolve(__dirname, './src'),
    },
  },
});

这样可以确保 Vitest 正确解析模块导入,避免因依赖缺失导致的错误。

更新 E2E 测试中的导入

将所有 E2E 测试中 import * as request from 'supertest' 的写法改为 import request from 'supertest'。这是因为当 Vitest 与 Vite 一起打包时,它期望 supertest 以默认导入的方式引入。在这个特定配置下,使用命名空间导入可能会导致问题。

最后,把 package.json 中的测试脚本更新为:

json
{
  "scripts": {
    "test": "vitest run",
    "test:watch": "vitest",
    "test:cov": "vitest run --coverage",
    "test:debug": "vitest --inspect-brk --inspect --logHeapUsage --threads=false",
    "test:e2e": "vitest run --config ./vitest.config.e2e.ts"
  }
}

这些脚本分别用于运行测试、监听文件变化、生成覆盖率报告和调试。test:e2e 脚本则专门用于使用自定义配置文件运行 E2E 测试。

完成以上配置后,你就可以在 NestJS 项目中享受到 Vitest 带来的好处,包括更快的测试执行速度,以及更现代的测试体验。

提示

你可以在这个仓库中查看一个可运行示例。

基于 NestJS 官方文档翻译