Fetching latest headlines…
Module System — Module Resolution
NORTH AMERICA
🇺🇸 United StatesJune 29, 2026

Module System — Module Resolution

1 views0 likes0 comments
Originally published byDev.to

Module resolution: cách runtime tìm file một import, và vì sao alias hay vỡ

Khi viết import { x } from './utils' hay import x from 'lodash', có một thuật toán quyết định file nào thực sự được load. Hiểu thuật toán này là điều kiện để gỡ lỗi "Cannot find module" — lỗi xuất hiện nhiều khi cấu hình path alias (@/components), vì alias do công cụ build hiểu, không phải Node hiểu sẵn, nên dễ vỡ khi một mắt xích (TypeScript, bundler, runtime, test) không được cấu hình khớp. Đây là nguồn của loại bug khó chịu "chạy được lúc dev, lỗi lúc build/test/production".

Cơ chế hoạt động

Node phân biệt hai loại đường dẫn. Đường dẫn tương đối/tuyệt đối (./, ../, /) resolve trực tiếp tới file, thử các đuôi (.js, .json...) và index. Đường dẫn "bare" (lodash, @scope/pkg) là package: Node leo các thư mục node_modules từ file hiện tại lên gốc, và trong mỗi package đọc package.json ("main", "exports", "module") để biết file vào.

import './utils'        // file: ./utils.js hoặc ./utils/index.js
import 'lodash'         // package: node_modules/lodash, đọc package.json
import '@/components/Button' // alias: KHÔNG phải cú pháp Node — cần công cụ map lại

Alias (@/...) không có ý nghĩa với Node thuần. Nó được khai trong tsconfig.json (compilerOptions.paths) cho TypeScript hiểu lúc compile, và phải được khai song song trong mọi công cụ khác chạm tới code: bundler (webpack resolve.alias, vite resolve.alias), test runner (Jest moduleNameMapper), và runtime nếu chạy thẳng. tsconfig paths chỉ dạy compiler — nó không tự đổi đường dẫn trong output, nên thiếu cấu hình ở tầng chạy là vỡ.

Vấn đề gặp trong production

Failure mode: alias chạy dev nhưng vỡ ở build/test/runtime. Đây là cái bẫy kinh điển. tsconfig.jsonpaths: { "@/*": ["src/*"] } nên editor và tsc không báo lỗi, nhưng:

- Jest không biết @/ -> test fail "Cannot find module '@/utils'"
- Bundler không cấu hình alias -> build fail
- Chạy file đã compile bằng node -> runtime "Cannot find module '@/utils'"

tsconfig paths chỉ phục vụ type-check, mỗi công cụ khác cần cấu hình alias riêng của nó. Bỏ sót một công cụ là một môi trường vỡ. Khi đổi alias, phải đổi đồng bộ ở tất cả các nơi.

Failure mode: sai alias trỏ tới nơi không tồn tại gây lỗi runtime. Một alias cấu hình lệch (trỏ src/ nhưng output ở dist/) compile sạch nhưng runtime không tìm thấy file. Vì lỗi chỉ xuất hiện lúc chạy chứ không lúc type-check, nó lọt qua tới khi thực thi.

Failure mode: case-sensitivity giữa máy dev và server. macOS không phân biệt hoa/thường trong tên file, Linux (server/CI) thì có. import './Button' trỏ file button.tsx chạy ngon trên Mac, vỡ "Cannot find module" trên CI/production Linux. Đây là một trong những lỗi "chỉ vỡ trên CI" hay gặp nhất.

Failure mode: exports field chặn deep import. Package hiện đại dùng "exports" trong package.json để khai các entry hợp lệ; import sâu vào file nội bộ (pkg/lib/internal) không được liệt kê sẽ bị chặn dù file tồn tại. Phải dùng entry point package công bố.

Cách debug và monitor

"Cannot find module" — bước đầu xác định loại: alias (@/) hay package hay tương đối. Nếu alias, kiểm tra alias đã khai ở mọi công cụ (tsconfig + bundler + test + runtime) chưa, và trỏ đúng thư mục output không. Nếu chỉ vỡ trên CI mà chạy được local, nghi case-sensitivity — kiểm tra hoa/thường tên file khớp chính xác import. Với package, kiểm tra package.json "exports" có cho phép đường dẫn đang import không. Bật forceConsistentCasingInFileNames trong tsconfig để bắt lỗi hoa/thường lúc compile thay vì để CI phát hiện. Khi cấu hình alias mới, chạy cả build, test, và runtime — không chỉ dev server — trước khi tin nó hoạt động.

Tradeoff

Path alias làm import dễ đọc và ổn định (@/components/Button thay vì ../../../components/Button), tránh chuỗi ../ dài và khỏi sửa import khi di chuyển file. Cái giá là tăng độ phức tạp build: phải cấu hình alias đồng bộ ở mọi công cụ, và một mắt xích thiếu là một môi trường vỡ — đúng loại lỗi khó chịu vì lệch giữa dev và build/CI. Quy tắc thực tế: dùng alias cho gốc dự án (@/) để tránh ../ sâu, nhưng giữ cấu hình tập trung và đồng bộ qua tất cả công cụ; bật forceConsistentCasingInFileNames; và luôn xác nhận alias hoạt động ở build/test/runtime chứ không chỉ editor. Với dự án nhỏ, import tương đối có khi đơn giản hơn là gánh đồng bộ alias.

Câu hỏi phỏng vấn

Node resolve một module như thế nào, và vì sao path alias chạy được lúc dev nhưng vỡ lúc build hoặc test?

Node phân biệt đường dẫn tương đối/tuyệt đối (./, /) — resolve trực tiếp tới file, thử các đuôi và index — với đường dẫn "bare" (lodash) — leo các node_modules từ file hiện tại lên gốc và đọc package.json (main/exports/module) để tìm entry. Path alias như @/components không phải cú pháp Node hiểu sẵn: nó được khai trong tsconfig.json paths để TypeScript hiểu lúc type-check, nhưng tsconfig paths không tự đổi đường dẫn trong output. Nên alias chạy được lúc dev (editor/tsc hiểu) nhưng vỡ ở build/test/runtime nếu các công cụ đó — bundler (resolve.alias), test runner (Jest moduleNameMapper), runtime — chưa được cấu hình alias riêng; mỗi mắt xích thiếu là một môi trường "Cannot find module". Điểm ăn điểm: nêu thêm case-sensitivity (chạy trên Mac vỡ trên CI Linux, bật forceConsistentCasingInFileNames), exports field chặn deep import, và quy tắc luôn kiểm alias ở build/test/runtime chứ không chỉ dev.

Hands-on

Cấu hình một path alias @/*src/* chỉ trong tsconfig.json và quan sát editor/tsc không báo lỗi, rồi chạy test (Jest) và build (vite/webpack) để thấy chúng fail "Cannot find module" — sau đó thêm alias tương ứng vào moduleNameMapperresolve.alias để cả ba môi trường cùng chạy. Đổi tên một file thành chữ thường nhưng giữ import viết hoa, chạy local trên Mac thấy OK rồi chạy trên một container Linux để tái hiện lỗi CI, và bật forceConsistentCasingInFileNames để bắt nó lúc compile. Cuối cùng thử deep-import vào một file nội bộ của một package có "exports" để gặp lỗi bị chặn, và sửa bằng cách dùng entry point công bố.

Comments (0)

Sign in to join the discussion

Be the first to comment!