---
url: https://docs.nemo12.com/engineering/dependency-hygiene.md
description: >-
  Kỷ luật nâng thư viện từ đợt rà 25.08.2026 (SRC-591): bốn lỗi đã xảy ra thật,
  như workspace nội bộ luôn là '*'.
---

# Kỷ luật phụ thuộc

Ghi từ đợt rà thư viện 2026-08-25 (SRC-591). Bốn điều dưới đây đều là lỗi **đã xảy ra thật** trong
chính đợt rà ấy, không phải lo xa.

## 1. Workspace nội bộ luôn là `"*"`

`npm update --save` đổi `"@nemo12/design-system": "*"` thành `"^0.1.0"` ở **10 `package.json`**.
Gói đó là `private`, không tồn tại trên registry — một dải phiên bản khiến npm đi tìm bên ngoài
thay vì dùng bản trong repo.

Điều làm nó nguy hiểm: **máy đã có `node_modules` thì không thấy gì cả**. Nó chỉ nổ ở lần cài sạch,
tức là ở CI, hoặc ở máy người mới. Cùng họ với bài học "check:docs local nói dối" trong CLAUDE.md.

Cổng: `scripts/check-deps.mjs`.

## 2. Một bản PATCH cũng đủ làm vỡ

`@hono/zod-openapi` **1.5.2 → 1.5.3** đổi kiểu `Handler` và làm đỏ **9 chỗ** typecheck ở
`workers/api`. Số hiệu phiên bản không hứa gì cả.

Vì thế gói này **ghim tuyệt đối** (`"1.5.2"`, không `^` không `~`). Muốn nâng thì nâng riêng nó,
chạy `npm run typecheck` ngay, và sửa code trong cùng lần — không nâng kèm một lệnh `update` hàng loạt.

Cách truy khi một lệnh `update` làm đỏ: `git stash` phần `package*.json` → `npm ci` → typecheck.
Sạch nghĩa là lỗi đến từ bản nâng, không phải từ code có sẵn.

> Ghim vào `package.json` chưa đủ nếu `package-lock.json` còn giữ nhánh cũ trong
> `workers/api/node_modules/...` — phải gỡ khoá đó khỏi lock rồi `npm install` lại.

## 3. Cấm `"latest"` cho gói ngoài

Build phải tái lập được: cùng một commit, cài lại lúc nào cũng ra cùng cây phụ thuộc. `"latest"`
phá đúng điều đó, và hỏng theo kiểu "hôm qua chạy được". Đã dọn `@vitejs/plugin-react` ở `apps/web`.

## 4. Lỗ hổng không phải lúc nào cũng vá được — và phải nói ra

`npm audit` báo 3 lỗ (2 moderate, 1 high) đến từ **VitePress 1.6.4** kéo theo `vite` cũ và `esbuild`
cũ. Bản 1.6.4 đã là mới nhất; đường vá duy nhất là `2.0.0-alpha`.

Quyết định: **không đưa alpha vào hai site tài liệu**. Lý do đứng vững vì lỗ này thuộc **dev server**
của esbuild — Pearl/Compass/Docs dựng tĩnh rồi đẩy lên Cloudflare, không chạy dev server ở đâu công
khai. Ghi ra đây để lần audit sau không ai phải điều tra lại từ đầu; xem lại khi VitePress 2 ổn định.

## 4b. Nâng bốn major — làm được nhờ tra tài liệu TRƯỚC (SRC-592)

Đợt 2026-08-26 nâng **ESLint 10 · TypeScript 7 · Vitest 4 · KaTeX 0.18**, lần này có Context7 nên
tra breaking change trước khi chạm vào `package.json`. Nhờ vậy biết ngay phần lớn không ảnh hưởng:

| Bản | Cái nó bỏ | Nemo12 có dính? |
| --- | --- | --- |
| TypeScript 7 | `moduleResolution: node/classic/node10`, `target: es5`, `module: amd/umd/system/none` | không — 35 tsconfig đều `bundler` + `ES2022` |
| TypeScript 7 | **`baseUrl` gỡ hẳn** | **có** — shadcn CLI vừa thêm hôm trước vào 6 tsconfig |
| Vitest 4 | option là tham số thứ BA của `test`/`describe`; `workspace`; `coverage.all`/`extensions` | không |
| KaTeX 0.17 | API nội bộ `__defineFunction` | không |
| ESLint 10 | (thêm rule) `no-useless-assignment` | **có** — 3 chỗ trong `scripts/` |

Hai thứ phải sửa và cách sửa:

* **`baseUrl`**: bỏ hẳn. TypeScript 7 giải `paths` tương đối với chính `tsconfig.json`, nên
  `"@/*": ["./src/*"]` vẫn đúng mà không cần `baseUrl`.
* **`no-useless-assignment`**: cả 3 chỗ cùng một mẫu — `let x = []; try { x = f(); } catch { continue; }`.
  Giá trị khởi tạo không bao giờ được đọc. Khai `let x;` là đúng ý hơn: *chưa có giá trị cho tới khi
  thử xong*. Rule này bắt đúng, không phải càu nhàu phong cách.

Cách làm đáng giữ cho lần sau: **tra tài liệu bản đích trước, đối chiếu với codebase bằng `grep`,
rồi mới nâng.** Nâng trước rồi đọc lỗi là cách chậm hơn và dễ hoảng.

## 5. Best practices đã rà trong đợt này

| Mặt | Kết quả |
| --- | --- |
| React 19 (`forwardRef`, `React.FC`, `defaultProps`, `useFormState`, ref callback trả ngầm) | sạch — 0 chỗ |
| `Context.Provider` → `<Context>` | sửa 1 chỗ (`apps/learn/src/lang.tsx`) |
| Tailwind v4 (config JS, `@tailwind`, class đã bỏ) | sạch — đã CSS-first từ đầu |
| `<img>` thiếu `alt` | sạch — 0 chỗ |
| `dangerouslySetInnerHTML` | 5 chỗ: 4 là KaTeX (bắt buộc, đầu vào do hệ sinh), 1 là **lỗ XSS thật** đã vá — xem dưới |

**Lỗ XSS đã vá.** `apps/marlins/src/ChildReport.tsx` đổi `**…**` thành `<strong>` rồi nhét qua
`dangerouslySetInnerHTML`. Câu ấy do server sinh nhưng **có nhúng dữ liệu thật**: tên Unit, và ở
các nhánh khác là tên trường mục tiêu — thứ phụ huynh tự gõ. Nay tách chuỗi rồi để React dựng phần
tử (`BoldText.tsx`); React tự thoát ký tự nên không còn đường nào để chữ biến thành mã.

**Test không chạy cũng là test không tồn tại.** `apps/marlins` có 3 file `.test.ts` nhưng
`package.json` **không có script `test`** — chúng chưa từng chạy lần nào. Bật lên: 11 test có thật.
Khi thêm test vào một workspace, kiểm luôn rằng `npm test` ở gốc có gọi tới nó.
