SWC (Speedy Web Compiler) là một platform mở rộng được viết bằng Rust, dùng được cho cả việc compile và bundle. Dùng SWC với Nest CLI là cách đơn giản và hiệu quả để tăng tốc đáng kể quá trình phát triển của bạn.
Gợi ý SWC nhanh hơn TypeScript compiler mặc định khoảng 20 lần.
Cài đặt
Để bắt đầu, trước tiên cài một vài package:
$ npm i --save-dev @swc/cli @swc/core
Bắt đầu (Getting started)
Sau khi cài đặt xong, bạn có thể dùng builder swc với Nest CLI như sau:
$ nest start -b swc
# OR nest start --builder swc
Gợi ý Nếu repo của bạn là monorepo, xem phần này.
Thay vì truyền flag -b, bạn cũng có thể đặt property compilerOptions.builder thành "swc" trong file nest-cli.json, như sau:
{
"compilerOptions": {
"builder": "swc"
}
}
Để tùy biến hành vi của builder, bạn có thể truyền một object gồm hai thuộc tính, type ("swc") và options, như sau:
{
"compilerOptions": {
"builder": {
"type": "swc",
"options": {
"swcrcPath": "infrastructure/.swcrc",
}
}
}
}
Ví dụ, để swc compile cả file .jsx và .tsx, làm như sau:
{
"compilerOptions": {
"builder": {
"type": "swc",
"options": { "extensions": [".ts", ".tsx", ".js", ".jsx"] }
},
}
}
Để chạy ứng dụng ở watch mode, dùng lệnh sau:
$ nest start -b swc -w
# OR nest start --builder swc --watch
Kiểm tra kiểu dữ liệu (Type checking)
SWC không tự thực hiện việc kiểm tra kiểu dữ liệu (type checking) (khác với TypeScript compiler mặc định), nên để bật tính năng này, bạn cần dùng flag --type-check:
$ nest start -b swc --type-check
Lệnh này yêu cầu Nest CLI chạy tsc ở chế độ noEmit song song với SWC, để thực hiện type checking một cách bất đồng bộ (asynchronously). Cũng giống như trên, thay vì truyền flag --type-check, bạn có thể đặt property compilerOptions.typeCheck thành true trong file nest-cli.json, như sau:
{
"compilerOptions": {
"builder": "swc",
"typeCheck": true
}
}
Ghi chú người dịch Đây là lý do SWC nhanh hơn tsc nhiều lần: nó chỉ strip type annotation và biên dịch cú pháp (giống Babel), không xây dựng cây kiểu dữ liệu đầy đủ để kiểm tra type như tsc. Vì vậy nếu chỉ dùng -b swc mà không thêm --type-check, code có lỗi type vẫn build và chạy được — lỗi chỉ lộ ra lúc runtime, khác hẳn với việc tsc chặn build ngay khi biên dịch.
CLI Plugin (SWC)
Flag --type-check sẽ tự động chạy các plugin của NestJS CLI và sinh ra một file metadata đã serialize, để ứng dụng có thể load file đó lúc runtime.
Cấu hình SWC
Builder SWC đã được cấu hình sẵn để phù hợp với yêu cầu của ứng dụng NestJS. Tuy nhiên, bạn có thể tùy biến cấu hình này bằng cách tạo file .swcrc ở thư mục gốc và chỉnh các option theo ý muốn.
{
"$schema": "https://swc.rs/schema.json",
"sourceMaps": true,
"jsc": {
"parser": {
"syntax": "typescript",
"decorators": true,
"dynamicImport": true
},
"baseUrl": "./"
},
"minify": false
}
Monorepo
Nếu repo của bạn là monorepo, thay vì dùng builder swc, bạn phải cấu hình webpack để dùng swc-loader.
Đầu tiên, cài package cần thiết:
$ npm i --save-dev swc-loader
Sau khi cài xong, tạo file webpack.config.js ở thư mục gốc của ứng dụng với nội dung sau:
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 và CLI plugin
Nếu bạn dùng CLI plugin, swc-loader sẽ không tự load chúng. Thay vào đó, bạn phải tạo một file riêng để load chúng bằng tay. Để làm điều này,
khai báo file generate-metadata.ts cạnh file main.ts với nội dung sau:
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: import.meta.dirname })],
outputDir: import.meta.dirname,
watch: true,
tsconfigPath: 'apps/<name>/tsconfig.app.json',
});
Gợi ý Trong ví dụ này chúng ta dùng plugin @nestjs/swagger, nhưng bạn có thể dùng bất kỳ plugin nào bạn muốn.
Phương thức generate() nhận các option sau:
watch |
Có theo dõi (watch) project để bắt thay đổi hay không. |
tsconfigPath |
Đường dẫn tới file tsconfig.json. Tương đối so với working directory hiện tại (process.cwd()). |
outputDir |
Đường dẫn tới thư mục sẽ lưu file metadata. |
visitors |
Một mảng các visitor được dùng để sinh metadata. |
filename |
Tên của file metadata. Mặc định là metadata.ts. |
printDiagnostics |
Có in diagnostic ra console hay không. Mặc định là true. |
Cuối cùng, bạn có thể chạy script generate-metadata trong một terminal riêng bằng lệnh sau:
$ npx ts-node src/generate-metadata.ts
# OR npx ts-node apps/{YOUR_APP}/src/generate-metadata.ts
Các cạm bẫy thường gặp
Nếu bạn dùng TypeORM/MikroORM hoặc ORM khác trong ứng dụng, bạn có thể gặp vấn đề về circular import. SWC không xử lý tốt circular import, vì vậy bạn nên dùng cách khắc phục sau:
@Entity()
export class User {
@OneToOne(() => Profile, (profile) => profile.user)
profile: Relation<Profile>; // <--- xem type "Relation<>" ở đây, thay vì chỉ "Profile"
}
Gợi ý Type Relation được export từ package typeorm.
Việc này giúp tránh lưu type của property vào code đã transpile trong property metadata, từ đó tránh được vấn đề circular dependency.
Nếu ORM bạn dùng không có cách khắc phục tương tự, bạn có thể tự định nghĩa wrapper type:
/**
* Wrapper type dùng để né vấn đề circular dependency của ESM module
* gây ra bởi việc reflection metadata lưu lại type của property.
*/
export type WrapperType<T> = T; // WrapperType === Relation
Với tất cả circular dependency injection trong project, bạn cũng cần dùng custom wrapper type như mô tả trên:
@Injectable()
export class UsersService {
constructor(
@Inject(forwardRef(() => ProfileService))
private readonly profileService: WrapperType<ProfileService>,
) {};
}
Ghi chú người dịch Vấn đề circular import này quen thuộc nếu bạn từng gặp circular dependency trong Go: Go compiler chặn cứng circular import giữa package ngay từ lúc build, còn ở đây SWC chỉ “quên” giữ type của property khi transpile (vì SWC transpile từng file độc lập, không phân tích toàn bộ project như tsc), khiến decorator metadata bị rỗng và NestJS resolve provider sai. Dùng Relation<T> hay WrapperType<T> chỉ là một type alias vô hại về runtime — nó đánh lừa reflection để không lưu type gốc, chứ không thực sự thay đổi logic.
Jest + SWC
Để dùng SWC với Jest, bạn cần cài các package sau:
$ npm i --save-dev jest @swc/core @swc/jest
Sau khi cài xong, cập nhật file package.json/jest.config.js (tùy theo cấu hình của bạn) với nội dung sau:
{
"jest": {
"transform": {
"^.+\\.(t|j)s?$": ["@swc/jest"]
}
}
}
Ngoài ra bạn cần thêm các property transform sau vào file .swcrc: legacyDecorator, decoratorMetadata:
{
"$schema": "https://swc.rs/schema.json",
"sourceMaps": true,
"jsc": {
"parser": {
"syntax": "typescript",
"decorators": true,
"dynamicImport": true
},
"transform": {
"legacyDecorator": true,
"decoratorMetadata": true
},
"baseUrl": "./"
},
"minify": false
}
Nếu bạn dùng NestJS CLI Plugin trong project, bạn sẽ phải chạy PluginMetadataGenerator bằng tay. Xem phần này để biết thêm.
Vitest
Vitest là một test runner nhanh và nhẹ, được thiết kế để hoạt động với Vite. Nó cung cấp một giải pháp test hiện đại, nhanh và dễ dùng, có thể tích hợp với các project NestJS.
Cài đặt
Để bắt đầu, trước tiên cài các package cần thiết:
$ npm i --save-dev vitest unplugin-swc @swc/core @vitest/coverage-v8
Cấu hình
Tạo file vitest.config.ts ở thư mục gốc của ứng dụng với nội dung sau:
import swc from 'unplugin-swc';
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
root: './',
},
plugins: [
// Cần thiết để build các file test bằng SWC
swc.vite({
// Đặt rõ module type để tránh kế thừa giá trị này từ file cấu hình `.swcrc`
module: { type: 'es6' },
}),
],
resolve: {
alias: {
// Đảm bảo Vitest resolve đúng TypeScript path alias
'src': resolve(import.meta.dirname, './src'),
},
},
});
File cấu hình này thiết lập môi trường Vitest, thư mục gốc, và plugin SWC. Bạn cũng nên tạo một file cấu hình riêng
cho e2e test, với thêm field include để chỉ định regex đường dẫn test:
import swc from 'unplugin-swc';
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
include: ['**/*.e2e-spec.ts'],
globals: true,
root: './',
},
plugins: [swc.vite()],
});
Ngoài ra, bạn có thể đặt option alias để hỗ trợ TypeScript path trong test:
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()],
});
Path alias
Khác với Jest, Vitest không tự resolve TypeScript path alias như src/. Điều này có thể gây lỗi resolve dependency khi test. Để khắc phục, thêm cấu hình resolve.alias sau vào file vitest.config.ts:
import { resolve } from 'node:path';
export default defineConfig({
resolve: {
alias: {
'src': resolve(import.meta.dirname, './src'),
},
},
});
Điều này đảm bảo Vitest resolve đúng các module import, tránh lỗi liên quan đến dependency bị thiếu.
Cập nhật import trong E2E test
Đổi các import trong E2E test dùng import * as request from 'supertest' thành import request from 'supertest'. Đây là cần thiết vì Vitest, khi được bundle bằng Vite, yêu cầu default import cho supertest. Dùng namespace import có thể gây lỗi trong setup cụ thể này.
Cuối cùng, cập nhật các test script trong file package.json thành:
{
"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"
}
}
Các script này cấu hình Vitest để chạy test, theo dõi thay đổi (watch), sinh báo cáo code coverage, và debug. Script test:e2e dùng riêng để chạy E2E test với file cấu hình tùy chỉnh.
Với setup này, bạn đã có thể tận hưởng lợi ích của việc dùng Vitest trong project NestJS, bao gồm chạy test nhanh hơn và trải nghiệm test hiện đại hơn.
Gợi ý Bạn có thể xem một ví dụ chạy được trong repo này