Đang tải...

Cách viết một thư viện JavaScript với Vite + tsdown + Rolldown + pnpm

14/05/2026
5 phút đọc
Cách viết một thư viện JavaScript với Vite + tsdown + Rolldown + pnpm
Bài viết này mình sẽ hướng dẫn tạo một thư viện JavaScript/TypeScript dùng pnpm, từ dev, test local, đến publish lên npm. Stack: **TypeScript**, **tsdown** (build với Rolld...

Bài viết này mình sẽ hướng dẫn tạo một thư viện JavaScript/TypeScript dùng pnpm, từ dev, test local, đến publish lên npm. Stack: TypeScript, tsdown (build với Rolldown), Vitest, pnpm, output ESM only, test local qua file: protocol.

1. Tổng quan stack

Thành phần Công cụ Vai trò
Package manager pnpm Quản lý dependency, publish
Build tsdown Bundle TypeScript → ESM, dùng Rolldown engine
Test Vitest Test runner nhanh, native ESM
Language TypeScript Type safety, auto generate .d.ts
Local test file: protocol Import lib như package thật mà không cần publish

2. Khởi tạo thư viện

mkdir my-lib
cd my-lib
pnpm init

package.json

{
 "name": "my-lib",
 "version": "0.0.1",
 "type": "module",
 "main": "./dist/index.mjs",
 "types": "./dist/index.d.mts",
 "exports": {
 ".": {
 "types": "./dist/index.d.mts",
 "import": "./dist/index.mjs"
 }
 },
 "files": ["dist"],
 "scripts": {
 "dev": "tsdown --watch",
 "build": "tsdown",
 "test": "vitest run",
 "test:watch": "vitest",
 "prepublishOnly": "pnpm build"
 },
 "packageManager": "pnpm@10.25.0"
}

Một số điểm quan trọng:

  • "type": "module" — toàn bộ project dùng ESM.
  • mainexports trỏ vào dist/index.mjs — tsdown mặc định xuất .mjs cho ESM output.
  • files: ["dist"] — chỉ publish thư mục dist, không push src/test/config lên npm.
  • prepublishOnly — tự động build trước khi publish, đảm bảo dist luôn mới nhất.

Cài dependencies

pnpm add -D tsdown typescript vitest unrun

unrun là peer dependency runtime của tsdown, cần cài tường minh.

3. Cấu hình TypeScript

tsconfig.json

{
 "compilerOptions": {
 "target": "ES2022",
 "module": "ESNext",
 "moduleResolution": "bundler",
 "strict": true,
 "esModuleInterop": true,
 "skipLibCheck": true,
 "declaration": true,
 "declarationDir": "./dist",
 "outDir": "./dist",
 "rootDir": "./src"
 },
 "include": ["src"],
 "exclude": ["node_modules", "dist"]
}
  • "moduleResolution": "bundler" — tương thích với tsdown/Rolldown.
  • "declaration": true — tạo .d.ts làm nguồn cho tsdown dùng với plugin rolldown-plugin-dts.

4. Cấu hình Build với tsdown

tsdown là bundler chuyên cho thư viện, built on Rolldown, của cùng team làm Vite.

tsdown.config.ts

import { defineConfig } from "tsdown";

export default defineConfig({
 entry: ["src/index.ts"],
 outDir: "dist",
 format: ["esm"],
 dts: true,
 clean: true,
});
  • format: ["esm"] — chỉ xuất ESM (.mjs).
  • dts: true — tự động generate type declarations qua rolldown-plugin-dts.
  • clean: true — xóa dist trước mỗi lần build.

.gitignore

node_modules
dist
*.log

Không commit dist/. Chỉ generate khi publish.

5. Viết code

src/index.ts

export function greet(name: string): string {
 return `Hello, ${name}!`;
}

export function sum(a: number, b: number): number {
 return a + b;
}

6. Viết test

vitest.config.ts

import { defineConfig } from "vitest/config";

export default defineConfig({
 test: {
 include: ["tests/**/*.test.ts"],
 },
});

tests/index.test.ts

import { describe, it, expect } from "vitest";
import { greet, sum } from "../src/index";

describe("greet", () => {
 it("returns greeting with name", () => {
 expect(greet("World")).toBe("Hello, World!");
 });
});

describe("sum", () => {
 it("adds two numbers", () => {
 expect(sum(1, 2)).toBe(3);
 });
});
pnpm test # chạy test 1 lần
pnpm test:watch # watch mode

7. Build

pnpm build

Output:

dist/
├── index.mjs ← bundle ESM
└── index.d.mts ← type declarations

8. Test local (giống như consumer thật)

Không cần publish lên npm để test. Dùng file: protocol.

Tạo project consumer

mkdir ../my-lib-test
cd ../my-lib-test
pnpm init

package.json của consumer

{
 "name": "my-lib-test",
 "private": true,
 "type": "module",
 "scripts": {
 "start": "tsx src/index.ts"
 },
 "dependencies": {
 "my-lib": "file:../my-lib"
 },
 "devDependencies": {
 "tsx": "^4.19.0",
 "typescript": "^5.8.0"
 }
}

"my-lib": "file:../my-lib" — pnpm tạo symlink từ my-lib-test/node_modules/my-libmy-lib/dist. Mỗi lần build lại lib, consumer dùng code mới ngay, không cần pnpm install lại.

src/index.ts của consumer

import { greet, sum } from "my-lib";

console.log(greet("a"));
console.log("1 + 2 =", sum(1, 2));

Chạy

pnpm install
pnpm start
Hello, a!
1 + 2 = 3

9. Quy trình dev hàng ngày

# Terminal 1 — lib
cd my-lib
pnpm dev # watch mode, auto build khi code thay đổi

# Terminal 2 — consumer
cd my-lib-test
pnpm start # chạy lại để test code mới

Hoặc không cần watch:

cd my-lib && pnpm build # build một lần
cd my-lib-test && pnpm start

10. Publish lên npm

cd my-lib

# Đảm bảo đã login
pnpm whoami # kiểm tra, nếu chưa: pnpm login

# Đảm bảo code sạch, test pass
pnpm test

# Publish (prepublishOnly sẽ tự chạy build)
pnpm publish

Lần publish đầu tiên nên thêm --access public nếu package không scoped:

pnpm publish --access public

Các lần sau tăng version rồi publish:

pnpm version patch # 0.0.1 → 0.0.2
# hoặc: minor (0.1.0), major (1.0.0)
pnpm publish

11. Cấu trúc thư mục cuối cùng

my-lib/
├── src/
│ └── index.ts
├── tests/
│ └── index.test.ts
├── package.json
├── tsconfig.json
├── tsdown.config.ts
├── vitest.config.ts
└── .gitignore

my-lib-test/
├── src/
│ └── index.ts
├── package.json

12. Một số lưu ý

  • tsdown đang phát triển nhanh — nếu gặp lỗi peer dependency như unmet peer typescript@^6.0.0, có thể bỏ qua hoặc cài thêm unrun nếu thiếu.
  • Không bundle dependencies — nếu lib có dependency bên ngoài, thêm vào package.json dependency và config tsdown.config.ts với external: true.
  • Muốn xuất CJS? Sửa format: ["esm", "cjs"] trong tsdown.config.ts, output sẽ thêm index.cjs + index.d.cts.
  • Scoped package? Đổi "name": "@scope/my-lib" trong package.json, publish với --access public.


    Cảm ơn bác bạn đã đọc bài viết!

📚 Nguồn: Viblo

Chia sẻ bài viết

Cần tư vấn?

Liên hệ với chúng tôi để được hỗ trợ

Liên hệ ngay

Bài viết liên quan

🤖 Building Social Games with AI — The Practitioner's Guide 📖
14/05/2026

🤖 Building Social Games with AI — The Practitioner's Guide 📖

> A comprehensive, opinionated, actionable guide for **using AI to build, ship, and operate social games** in the lineage covered by [🌾 The Social Games Playbook 🎮](https://dev.to/truongpx396/th...

Đọc thêm
qs88nl5001
14/05/2026

qs88nl5001

qs88 mang đến sân chơi giải trí trực tuyến chuyên nghiệp với hệ thống vận hành ổn định cùng tốc độ xử lý nhanh chóng. Người tham gia dễ dàng tiếp cận h...

Đọc thêm
Claude đã giúp lưu lượng truy cập website của tôi tăng gấp đôi như thế nào ?
14/05/2026

Claude đã giúp lưu lượng truy cập website của tôi tăng gấp đôi như thế nào ?

> Bài viết này được tổng hợp và diễn giải lại từ bài gốc ["Claude is Doubling My Website Traffic"](https://nick-nolan.medium.com/claude-is-doubling-my-website-traffic-1a26a793b...

Đọc thêm

Bắt đầu dự án của bạn

Hãy để Flash Dev đồng hành cùng bạn

Liên hệ ngay