· 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

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.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ạy npx jsr publish)
  • Source code chỉ dùng ESM — không dùng require(), không dùng module.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:

FieldBắt buộcGhi chú
namePhải bắt đầu bằng @scope/. Ví dụ: @luca/greet.
versionSemVer hợp lệ: 1.0.0, 2.3.0-beta.1.
exportsMột path đơn (string) hoặc export map có tên (object). Key . là entry point mặc định.
publish.includeKhôngGlob pattern chỉ định file cần include. Ghi đè .gitignore.
publish.excludeKhôngGlob 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.

  1. Trong cài đặt JSR package của bạn, liên kết GitHub repository.
  2. 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ụcTiêu chí đánh giá
DocumentationCó README; JSDoc ở cấp module; JSDoc trên toàn bộ hàm và type được export
Best PracticesKhông có slow types trong export; package được publish với OIDC provenance
DiscoverabilityPackage 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:

  1. Thêm README.md kèm ví dụ sử dụng
  2. Viết JSDoc comment ở cấp module tại đầu file entry point chính
  3. Thêm JSDoc cho mỗi hàm, class và type được export
  4. Khai báo tường minh type cho toàn bộ export (loại bỏ slow types)
  5. Publish qua GitHub Actions với quyền id-token: write
  6. 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: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ạiTạo scope trên jsr.io/new trước — CLI không thể tự tạo scope
File bị từ chối khi publishTrá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 publishChỉ GitHub Actions OIDC mới tạo ra provenance; publish bằng token không bao giờ có
Điểm thấp dù đã có documentationKiể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-typesThay 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

Tài liệu tham khảo