Skip to content

Quy ước code

Nguồn duy nhất của quy ước là CLAUDE.md ở gốc repo. Hai mục dưới đây nhúng thẳng từ file đó lúc build docs — sửa quy ước thì sửa CLAUDE.md (theo CODEOWNERS, cần core team duyệt), trang này tự cập nhật. Claude Code của dev và job AI review trên MR cũng đọc đúng file này, nên người và AI làm việc theo cùng một bộ luật.

Quy tắc kiến trúc

Vi phạm là lý do từ chối MR.

  • Không import ant-design-vue trực tiếp ngoài @tasco/ui (layer được miễn). App/sản phẩm chỉ dùng C*, kể cả không viết thẻ <a-*> trong template (ESLint chưa bắt được).
  • composables không phụ thuộc ui; utils không phụ thuộc Nuxt.
  • Tối đa 2 tầng Nuxt layer; khoá chặt version nuxt / ant-design-vue.
  • Màu và spacing chỉ dùng var(--tasco-*); token mới phải thêm vào @tasco/theme trước.
  • Core chỉ giữ thứ thực sự dùng chung — business logic của một sản phẩm không được vào core.
  • Đổi core ảnh hưởng nhiều team → mở RFC trước (docs/core/rfc.md).
  • Không hardcode hay log token, secret, password.

Quy ước viết code

Chung

  • TypeScript strict + noUncheckedIndexedAccess + verbatimModuleSyntax. Cấm any (kể cả trong template), @ts-ignore, eslint-disable, enum. Chỉ dùng !/as khi đã chứng minh được là đúng.
  • Format: không ;, nháy đơn, thụt 2 space, trailing comma khi xuống dòng, số lớn viết 10_000. Comment giải thích vì sao; API public có JSDoc.
  • Import:
    • Dùng import type. Thứ tự: thư viện ngoài (node: trước) → @tasco/* → đường dẫn tương đối.
    • Package thư viện import tường minh từ vue/nuxt/app/h3; layer và app dùng auto-import.

Đặt tên

  • Kiểu tên:
    • camelCase cho biến/hàm; UPPER_SNAKE cho hằng module (regex thêm hậu tố _RE).
    • Type viết PascalCase, không tiền tố I*. interface cho object, type cho union/function. Tên dạng <Tên>Options/<Tên>Result.
    • Dùng union chuỗi thay enum; class chỉ dành cho Error.
  • Tiền tố hàm:
    • use composable · create factory · define giữ literal type
    • is/has kiểm tra · to chuyển đổi · fetch gọi HTTP · load nạp dữ liệu
    • map đổi dữ liệu ngoài → nội bộ · parse/sanitize xử lý input · format tạo chuỗi hiển thị · merge gộp
    • *Of truy xuất thuộc tính · on* handler · _reset* chỉ dùng cho test
  • Thương hiệu: Tasco trong tên biến/hàm; tasco trong chuỗi (tasco:<khu>:<tên>, $tasco<Tên>, --tasco-*).
  • File:
    • C<Pascal>.vue, use<Pascal>.ts; tên nhiều từ dùng kebab-case.
    • Plugin NN.<tên>.ts, middleware <tên>.global.ts, route <tên>.<method>.ts.
    • Test và story đặt cạnh file nguồn.
  • Vue: prop đặt tên show*/*Text/default*, v-model qua update:<prop>, emit là động từ viết thường. CSS theo BEM c-<tên>__phần--biến-thể (trong layer: tasco-<tên>).
  • Dữ liệu: DTO dùng camelCase. Dữ liệu từ ngoài (snake_case của OAuth/OpenAI) giữ trong type riêng và map sang ở BFF. Env đặt dạng NUXT_<SECTION>_<KEY>.

Lỗi & log

  • Client: mọi lỗi quy về AppError qua toAppError(). Mã HTTP nằm ở field status (không phải statusCode). Phân loại bằng isAuthError/isForbidden/isValidation/isServerError/isNetworkError.
  • Hiển thị lỗi qua useErrorHandler().handleError(): 401 → về trang login, 422 không bật toast, 5xx/mất mạng → notification, còn lại → message. Caller muốn tự xử lý thì gọi $tascoError(e).
  • Composable: bật loading → gán error.value = toAppError(e) → tắt loading trong finally. Không để promise bị reject mà không có ai bắt. Hàm parse trả null; hàm validate trả { ok, payload | reason }.
  • Server: throw createError({ statusCode, statusMessage: '<tiếng Việt>' }).
    • 400 input sai · 401 xác thực/IAM từ chối (code !== 'API000') · 403 không đủ quyền · 500 thiếu cấu hình (nêu tên env) · 502 lỗi upstream.
    • $fetch gọi ra ngoài luôn có timeout (10s).
  • Lỗi do dùng sai API: new Error('[@tasco/<pkg>] … <cách sửa>'). Không dùng console.log ở runtime; server chỉ dùng console.warn/error, có tiền tố tên package.

API & DTO

  • Route BFF trả object có key đặt tên ({ user: AuthUser | null }). Không có dữ liệu thì trả null, không trả 404.
  • Danh sách: Paginated<T> = { items, total }; query TableQuery { page (tính từ 1), pageSize, sortField?, sortOrder?, filters? }.
  • 422: field errors nằm ở gốc body { message, errors: { field: string[] } }. Không dùng createError({ data }), vì dữ liệu bị lồng vào datagetFieldErrors không đọc được.
  • /api/** proxy nguyên trạng. Backend lệch format thì chuyển đổi trong fetcher. Không trả envelope IAM/OIDC thẳng cho client.
  • DTO là interface trong types.ts: field tuỳ chọn dùng ?:, "không có" dùng T | null, input chưa tin cậy nhận unknown rồi kiểm tra bằng type guard, forward đi đâu thì chỉ giữ các field được phép.

Patterns

  • Composable-first: use* trả object phẳng. Không dùng class service/repository hay Pinia; fetcher gọi qua useApi đóng vai "repository".
  • Lõi thuần (không phụ thuộc Nuxt/Vue/h3) + lớp adapter I/O mỏng; test lõi thuần trực tiếp.
  • Dependency injection không cần container:
    • Plugin provide + useNuxtApp(); interface làm strategy (AuthProvider).
    • Truyền fetcher/callback vào (useTable(fetcher), useErrorHandler({ onAuthError })).
    • Cấu hình qua runtimeConfig/app.config.ts. Options là tham số cuối, mặc định = {}.
  • Factory đặt tên create*/define*; tách code client/server bằng subpath export.

Công cụ nào bắt quy ước nào

Quy ướcBắt tự động bằngChạy ở
Không import ant-design-vue ngoài @tasco/uiESLint tasco/no-direct-antdv — chặn cả import typepnpm lint
import type cho import chỉ dùng kiểuESLint @typescript-eslint/consistent-type-importspnpm lint
Không để biến thừaESLint @typescript-eslint/no-unused-vars (cho phép tiền tố _)pnpm lint
strict, noUncheckedIndexedAccess, verbatimModuleSyntax@tasco/tsconfig/base + tsc/vue-tscpnpm typecheck
Mô tả cho mọi prop, event, slot của C*; mỗi component có trang docsscripts/docs-coverage.mjs --strictjob quality
Khoá runtimeConfig của layer có trong trang tra cứuscripts/docs-coverage.mjs --strictjob quality
TSDoc cho export của composables, utils, theme, aidocs/.vitepress/gen-api.mjs --strictpnpm build (docs)
Tương phản các cặp màu tokenTest contrast-pairs.test.ts của @tasco/themepnpm test
Ngưỡng coverageVitest coverage.thresholds từng packagepnpm test
Không publish package chưa versionscripts/release-guard.mjsjob release

Chưa có công cụ, người review phải để ý:

  • Format (không ;, nháy đơn, trailing comma, 10_000): repo chưa bật rule format trong ESLint và chưa dùng Prettier — giữ đúng như code xung quanh.
  • Thẻ <a-*> viết trong template của app sản phẩm (ESLint chỉ chặn import).
  • Business logic của một sản phẩm lọt vào core; tên hàm sai tiền tố (use, create, to, is…).
  • Comment giải thích cái gì thay vì vì sao; comment không phải tiếng Việt.

Đổi quy ước

  1. Quy ước mới áp cho nhiều package hoặc đổi thói quen cả team → mở RFC trước.
  2. Sửa CLAUDE.md trong MR; quy ước có thể kiểm tự động thì thêm luôn rule ESLint, test hoặc bước kiểm trong script.
  3. Code hiện có chưa theo quy ước mới mà chưa sửa kịp thì ghi vào Nợ kỹ thuật.

AGENTS.md là bản rút gọn cho các AI agent khác — đổi quy ước quan trọng thì đồng bộ cả file đó.

Liên quan