· jsr / deno / typescript
Cách publish TypeScript package lên JSR: hướng dẫn chi tiết
Hướng dẫn từng bước để publish TypeScript package lên JSR: cấu hình jsr.json, khắc phục slow types, publish qua Deno hoặc GitHub Actions, đạt điểm score tối đa.
Bởi Ethan
1.903 từ · 10 phút đọc
Nếu bạn viết TypeScript library, JSR đáng để thử. Bạn push .ts source thẳng lên, JSR tự compile ra .js và .d.ts, và package của bạn xuất hiện trên cả JSR registry lẫn npm scope @jsr — không cần bước build riêng. Bài viết này đi qua toàn bộ quy trình publish — từ tạo tài khoản đến đạt điểm JSR score 100 — kèm theo pattern dual-publish cho những team cần giữ khả năng tương thích với npm.
Bài này dành cho ai
TypeScript developer đang publish một library mới, hoặc Node.js library author muốn hỗ trợ người dùng Deno và Bun mà không cần duy trì một build pipeline riêng. Nếu bạn đang dùng CommonJS và chưa thể migrate sang ESM, JSR chưa phù hợp với bạn — hãy quay lại khi đã chuyển xong.
Điều kiện trước khi bắt đầu
- Tài khoản JSR tại jsr.io (đăng nhập bằng GitHub hoặc Google)
- Deno ≥ 1.42 (
deno --version) hoặc Node.js + npm (để chạynpx jsr publish) - Source code chỉ dùng ESM — không dùng
require(), không dùngmodule.exports
Tạo scope và package
Scope là namespace của bạn trên JSR, ví dụ @yourname hoặc @yourorg. Vào jsr.io/new, tạo scope trước, sau đó tạo package bên trong scope đó.
Quy tắc đặt tên scope: 2–20 ký tự chữ thường, chỉ dùng chữ cái, số và dấu gạch nối. Package name áp dụng quy tắc tương tự.
Bạn phải tạo scope trên jsr.io trước khi chạy bất kỳ lệnh publish nào — CLI sẽ báo lỗi nếu scope chưa tồn tại.
Cấu hình jsr.json
Mỗi JSR package cần có file jsr.json (hoặc jsr.jsonc) ở thư mục gốc của repo. Nếu bạn dùng Deno, có thể gộp các field này vào deno.json thay thế.
Cấu hình tối thiểu:
{
"name": "@your-scope/your-package",
"version": "1.0.0",
"exports": "./mod.ts"
}
Với nhiều entry point và publish filter:
{
"name": "@acme/utils",
"version": "2.1.0",
"exports": {
".": "./mod.ts",
"./strings": "./src/strings.ts",
"./numbers": "./src/numbers.ts"
},
"publish": {
"include": ["LICENSE", "README.md", "src/**/*.ts"],
"exclude": ["src/tests", "**/*.test.ts"]
}
}
Tham chiếu các field:
| Field | Bắt buộc | Ghi chú |
|---|---|---|
name | Có | Phải bắt đầu bằng @scope/. Ví dụ: @luca/greet. |
version | Có | SemVer hợp lệ: 1.0.0, 2.3.0-beta.1. |
exports | Có | Một path đơn (string) hoặc export map có tên (object). Key . là entry point mặc định. |
publish.include | Không | Glob pattern chỉ định file cần include. Ghi đè .gitignore. |
publish.exclude | Không | Glob pattern chỉ định file cần exclude. |
Thêm dòng này vào jsr.json để có autocomplete trong editor:
{
"$schema": "https://jsr.io/schema/config-file.v1.json"
}
Viết code tương thích với JSR
Cách viết import
JSR package bắt buộc dùng import specifier tường minh kèm phần mở rộng file cho tất cả relative import:
// JSR packages
import { encodeBase64 } from "jsr:@std/encoding@1/base64";
// npm packages
import { cloneDeep } from "npm:lodash@4";
// Node.js built-ins
import { readFile } from "node:fs";
// Relative imports — phần mở rộng là bắt buộc
import { greet } from "./greet.ts";
Những gì không được phép: require(), module.exports, export =, module augmentation qua declare global, và relative import không có phần mở rộng.
Khắc phục slow types
Slow types là các TypeScript export mà JSR không thể trích xuất type mà không chạy toàn bộ TypeScript compiler. Chúng làm chậm quá trình type-check của người dùng thư viện và phá vỡ lớp tương thích npm của JSR.
Ba vi phạm phổ biến nhất — và cách khắc phục:
// SAI — kiểu trả về được suy luận
export function foo() {
return Math.random().toString();
}
// ĐÚNG — kiểu trả về tường minh
export function foo(): string {
return Math.random().toString();
}
// SAI — property class được suy luận
export class MyClass {
prop = computeSomething();
}
// ĐÚNG — kiểu property tường minh
export class MyClass {
prop: string = computeSomething();
}
// SAI — hằng số được suy luận
export const GLOBAL_ID = crypto.randomUUID();
// ĐÚNG — annotation kiểu tường minh
export const GLOBAL_ID: string = crypto.randomUUID();
Kiểm tra nhanh: nếu code của bạn compile sạch với isolatedDeclarations: true của TypeScript, thì không có slow types.
deno publish --allow-slow-types là lối thoát khẩn cấp, nhưng dùng nó sẽ làm giảm điểm JSR score ở mục Best Practices và làm kém trải nghiệm type cho người dùng qua npm. Đừng dùng trong production.
Publish package
Luôn chạy dry run trước
deno publish --dry-run
# hoặc
npx jsr publish --dry-run
--dry-run kiểm tra toàn bộ quy tắc mà không upload gì lên. Các version trên JSR là bất biến — sau khi đã publish, version đó không thể ghi đè hay xóa. Hãy chạy dry-run trước mỗi lần release.
Publish từ máy local
deno publish
# hoặc
npx jsr publish
Cả hai lệnh đều mở một cửa sổ trình duyệt để xác thực. Bạn xác nhận quyền publish cho scope và version cụ thể. CLI không lưu trữ credential nào.
Publish từ GitHub Actions (khuyến nghị)
Cách dùng GitHub Actions được ưu tiên hơn vì nó tạo ra SLSA provenance attestation qua Sigstore, hiển thị trong mục Provenance ở cuối trang overview của package — và góp phần vào điểm Best Practices.
- Trong cài đặt JSR package của bạn, liên kết GitHub repository.
- Tạo
.github/workflows/publish.yml:
name: Publish to JSR
on:
push:
tags:
- "v*"
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # bắt buộc cho OIDC provenance
steps:
- uses: actions/checkout@v4
- run: npx jsr publish
Permission id-token: write là thứ kích hoạt OIDC. Publish bằng token — dù có trả phí — không bao giờ tạo ra provenance attestation.
Nếu bạn đang so sánh các CI provider cho release pipeline, GitHub Actions vs CircleCI phân tích chi tiết ưu nhược điểm của từng lựa chọn.
Publish bằng token (CI không có OIDC)
# Tạo token trong JSR Account Settings → Tokens
# Lưu vào repository secret, sau đó:
npx jsr publish --token $JSR_TOKEN
Cách này hoạt động nhưng bỏ qua provenance. Dùng OIDC khi có thể.
Tối ưu điểm JSR score
Mỗi package được chấm điểm từ 0–100 dựa trên bốn mục. Điểm này hiển thị công khai trên trang package và ảnh hưởng đến thứ hạng tìm kiếm.
| Mục | Tiêu chí đánh giá |
|---|---|
| Documentation | Có README; JSDoc ở cấp module; JSDoc trên toàn bộ hàm và type được export |
| Best Practices | Không có slow types trong export; package được publish với OIDC provenance |
| Discoverability | Package có description không trống |
| Compatibility | Ít nhất một runtime được đánh dấu “compatible” trong cài đặt package |
Các bước để đạt điểm tối đa:
- Thêm
README.mdkèm ví dụ sử dụng - Viết JSDoc comment ở cấp module tại đầu file entry point chính
- Thêm JSDoc cho mỗi hàm, class và type được export
- Khai báo tường minh type cho toàn bộ export (loại bỏ slow types)
- Publish qua GitHub Actions với quyền
id-token: write - Vào cài đặt package trên jsr.io và đánh dấu runtime tương thích (Deno, Node.js, Bun, browser)
@std/encoding từ Deno Standard Library là tham khảo hữu ích: package này luôn đạt gần 100 điểm và cho thấy documentation đầy đủ trông như thế nào trong thực tế.
Lớp tương thích npm
Bạn không cần bước publish riêng cho người dùng npm. Mỗi JSR package đều có sẵn qua npm scope @jsr — nhưng bạn cần cấu hình registry cho package manager trước.
Cách khuyến nghị là dùng npx jsr add, lệnh này tự động ghi entry .npmrc:
npx jsr add @std/encoding
npm và Bun không tự cấu hình scope @jsr; chạy thẳng npm install @jsr/std__encoding mà không có setup sẽ trả về lỗi E404. pnpm, yarn và vlt tự cấu hình tự động. Nếu muốn cấu hình thủ công, thêm một dòng vào .npmrc:
@jsr:registry=https://npm.jsr.io
Sau đó dùng lệnh install thông thường:
# npm (cần entry .npmrc ở trên)
npm install @jsr/std__encoding
# Bun (cần entry .npmrc ở trên)
bun add @jsr/std__encoding
# pnpm (tự cấu hình registry)
pnpm add @jsr/std__encoding
Quy tắc đổi tên: @scope/name trên JSR → @jsr/scope__name trên npm (chú ý dấu gạch dưới kép).
Nếu bạn đang chọn package manager để cài các package này, pnpm vs npm so sánh chi tiết về tốc độ cài đặt, disk space và hỗ trợ monorepo.
Bên dưới, JSR tự động rewrite các specifier jsr: và npm:, transpile TypeScript sang .js, và generate file .d.ts — tất cả đều ở phía server, không cần chạy tsc. Kết quả được publish dưới dạng npm tarball tiêu chuẩn trong scope @jsr.
Dual-publish lên JSR và npm
Nếu bạn đã có người dùng trên npm, hoặc muốn tiếp tục dùng import ... from "your-package" mà không cần tiền tố @jsr/, bạn có thể publish độc lập lên cả hai registry.
Nên dual-publish khi: bạn viết utility không phụ thuộc runtime (parsing, validation, formatting, hashing, logic thuần) và muốn hỗ trợ Deno + Node.js + Bun + browser.
Nên bỏ qua npm khi: code dùng API Deno.* mà Node.js không có.
Cấu hình dual-publish duy trì song song hai file config:
jsr.json — TypeScript source, không cần bước build:
{
"name": "@acme/mylib",
"version": "1.2.0",
"exports": "./src/index.ts"
}
package.json — output đã compile sau bước build:
{
"name": "mylib",
"version": "1.2.0",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
}
}
}
GitHub Actions workflow:
- run: npx jsr publish # TypeScript source → JSR
- run: npm run build && npm publish # compiled JS → npm
Giữ version đồng bộ thủ công hoặc dùng release-please. Một lợi ích ít ai chú ý: validation chặt hơn của JSR thường bắt được các lỗi export map mà npm im lặng bỏ qua.
@hono/hono là ví dụ thực tế tốt nhất cho pattern này — team Hono publish lên cả hai registry và dùng JSR làm nguồn chính thức.
Những lỗi thường gặp
| Vấn đề | Cách xử lý |
|---|---|
deno publish báo lỗi scope không tồn tại | Tạo scope trên jsr.io/new trước — CLI không thể tự tạo scope |
| File bị từ chối khi publish | Tránh dùng :, *, ?, <, >, |, \ trong tên file/thư mục — JSR yêu cầu đường dẫn tương thích với Windows |
| Không thấy tab Provenance sau khi publish | Chỉ GitHub Actions OIDC mới tạo ra provenance; publish bằng token không bao giờ có |
| Điểm thấp dù đã có documentation | Kiểm tra xem đã cài đặt runtime compatibility trong cài đặt package chưa — mục này dễ bỏ sót |
Cần dùng --allow-slow-types | Thay vào đó hãy khai báo tường minh type cho toàn bộ export; slow types làm xấu trải nghiệm của người dùng qua npm |