การจัดการ Dependencies
package.json แยก dependency ออกเป็นหลายส่วนตามจุดประสงค์การใช้งาน การเข้าใจความแตกต่างช่วยให้ production bundle มีขนาดเล็กลง ป้องกัน peer conflict และทำให้ CI builds มีความน่าเชื่อถือ
dependencies
หัวข้อที่มีชื่อว่า “dependencies”dependencies คือ package ที่โค้ดของคุณต้องการเมื่อ ทำงานจริง เมื่อผู้ใช้ติดตั้ง package ของคุณ npm จะ install dependencies เหล่านี้ไปด้วย
npm install expressคำสั่งนี้เพิ่ม express เข้าไปในส่วน "dependencies" ของ package.json:
{ "dependencies": { "express": "^4.18.2" }}devDependencies
หัวข้อที่มีชื่อว่า “devDependencies”devDependencies คือ package ที่ต้องใช้เฉพาะระหว่าง การพัฒนา เช่น test runner, linter, bundler หรือ type definitions ไม่ถูก install เมื่อผู้ใช้ติดตั้ง package ของคุณ และไม่ถูก install ใน environment production เมื่อรัน npm ci --omit=dev
npm install --save-dev eslint vitest typescript{ "devDependencies": { "eslint": "^8.57.0", "typescript": "^5.4.5", "vitest": "^1.6.0" }}peerDependencies
หัวข้อที่มีชื่อว่า “peerDependencies”peerDependencies ระบุ package ที่ ผู้ใช้ต้องจัดหาเอง นี่คือรูปแบบที่ถูกต้องสำหรับ plugin, framework integration และ shared library ที่ไม่ควรถูก bundle แยกไว้ในทุก package ที่ใช้มัน
{ "name": "eslint-plugin-my-rules", "version": "1.0.0", "peerDependencies": { "eslint": ">=8.0.0" }}ตั้งแต่ npm 7 เป็นต้นมา npm install จะ ติดตั้งให้อัตโนมัติ เมื่อ peer dependency ที่ขาดไปสามารถ resolve version ที่ตรงกับ range ได้ — ไม่ต้องมีขั้นตอนหรือ flag เพิ่มเติม npm จะแจ้งเตือน peer dependency ก็ต่อเมื่อไม่สามารถ resolve ได้อัตโนมัติเท่านั้น (เช่น version ที่ติดตั้งอยู่แล้วขัดกับ range ที่ต้องการ) ตัว plugin เองไม่เคยรวมสำเนา eslint ของตัวเองมาด้วย — มันใช้ตัวที่อยู่ในโปรเจกต์ของผู้ใช้
Lockfiles
หัวข้อที่มีชื่อว่า “Lockfiles”npm สร้าง package-lock.json เมื่อรัน npm install ครั้งแรก lockfile นี้ระบุ version ที่แน่นอนของทุก dependency (รวมถึง dependency ลำดับที่สอง) เพื่อให้ทุกคนในทีมและ CI ได้รับ dependency tree ที่เหมือนกันทุกประการ
{ "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTg==" }}Lockfile entry แต่ละอันมีฟิลด์ integrity ด้วย — hash แบบ base64 ของ byte ของ tarball จริงที่ npm resolve มา นี่คือสิ่งที่ทำให้ npm ci เป็น security boundary ไม่ใช่แค่ version pin: การ re-download ที่ได้ byte ต่างออกไป (registry ที่ถูก compromise, การส่งที่เสียหาย, mirror ที่ถูกแก้ไข) จะ fail การตรวจ hash แทนที่จะติดตั้งไปอย่างเงียบๆ
Output (verified on Node 24)
matches lockfile: true
tampered matches lockfile: falsenpm install เทียบกับ npm ci
หัวข้อที่มีชื่อว่า “npm install เทียบกับ npm ci”# npm install — resolves and potentially updates the lockfilenpm install
# npm ci — installs exactly what is in the lockfile; fails if package.json# and package-lock.json are out of sync; always starts with a clean slatenpm ciใช้ npm install ระหว่างพัฒนาเมื่อต้องการอัปเดต dependency ใช้ npm ci ใน CI/CD และ Docker build เพื่อให้ได้ reproducible install ที่เร็วและปลอดภัย
คำกล่าวอ้าง “fail อย่างชัดเจน” ของ npm ci ไม่ใช่ทฤษฎี — นี่คือ transcript จริง โปรเจกต์หนึ่งติดตั้ง lockfile แล้ว package.json drift ออกไป (เพิ่ม dependency ด้วยมือโดยไม่ได้รัน npm install ใหม่):
npm cinpm error code EUSAGEnpm errornpm error `npm ci` can only install packages when your package.json and package-lock.json or npm-shrinkwrap.json are in sync. Please update your lock file with `npm install` before continuing.npm errornpm error Missing: [email protected] from lock filenpm error Missing: [email protected] from lock fileมันไม่พยายาม reconcile หรือติดตั้งบางส่วนเลย — มัน refuse ทั้งหมด นี่คือ failure mode ที่ควรจับ lockfile ที่ล้าสมัยได้ใน CI ก่อนที่ dependency tree ที่แย่จะไปถึง production
Anti-pattern ของ Dockerfile
หัวข้อที่มีชื่อว่า “Anti-pattern ของ Dockerfile”จุดที่ความแตกต่างนี้ถูกทำผิดบ่อยที่สุดคือ Dockerfile การใช้ npm install ใน image build ทำลายจุดประสงค์ทั้งหมดของ lockfile — image build แต่ละครั้งสามารถ resolve transitive version ต่างกันเล็กน้อย แม้จาก package.json เดียวกันเป๊ะ:
# Anti-pattern: npm install in a Docker imageFROM node:24-slimWORKDIR /appCOPY package.json package-lock.json ./RUN npm install --omit=devCOPY . .CMD ["node", "server.js"]# Correct: npm ci pins the exact tree the lockfile recordedFROM node:24-slimWORKDIR /appCOPY package.json package-lock.json ./RUN npm ci --omit=devCOPY . .CMD ["node", "server.js"]npm ci --omit=dev ยังลบ node_modules ก่อนติดตั้งด้วย ดังนั้น layer เก่าจาก build ก่อนหน้าจึงไม่มีทาง leak package dev-only (หรือแย่กว่านั้นคือ version อื่นของ dependency จริง) เข้าไปใน production image ได้ npm install ใน Dockerfile คือบั๊กจริง ไม่ใช่แค่เรื่อง style: build สองครั้งจาก source tree เดียวกันเป๊ะ ห่างกันหลายวัน สามารถสร้าง image ที่ต่างกัน ไม่ reproducible ได้
overrides
หัวข้อที่มีชื่อว่า “overrides”overrides บังคับ version ที่กำหนดสำหรับ dependency ที่ไหนก็ตามที่มันปรากฏใน tree รวมถึงที่ซ้อนอยู่ภายใน dependency ที่คุณไม่ได้ควบคุม — เครื่องมือสำหรับ transitive package ที่ถูก pin ไว้ที่ version เก่าที่มีช่องโหว่ ที่ maintainer ของมันเองยังไม่ bump
{ "overrides": { "minimatch": "^3.1.2" }}Before/after จริง capture มาจากโปรเจกต์ที่มี direct dependency ตัวเดียวที่ตัวมันเอง pin minimatch เก่าที่มีช่องโหว่แบบตรงเป๊ะ:
npm ls minimatch[email protected] /path/to/overrides-demo└─┬ [email protected] -> ./outer{ "overrides": { "minimatch": "^3.1.2" }}npm install && npm ls minimatch[email protected] /path/to/overrides-demo└─┬ [email protected] -> ./outerminimatch transitive ย้ายจาก 3.0.4 ไปเป็น 3.1.5 โดยไม่ต้องแตะ outer-lib เลย overrides บังคับ version ที่ขัดกับ peer requirement ของ dependency เองไม่ได้ — npm ยังตรวจ compatibility อยู่ — แต่มันคือเครื่องมือที่ถูกต้องสำหรับสถานการณ์แบบนี้เป๊ะ ที่การรอให้ maintainer ทุกคนตลอดสาย publish bump ไม่ใช่ตัวเลือก
npm audit
หัวข้อที่มีชื่อว่า “npm audit”npm audit ตรวจ dependency ที่ติดตั้งกับ npm security advisory database และรายงานช่องโหว่ที่รู้จัก พร้อม severity และวิธีแก้ที่แนะนำ ผลลัพธ์จริงบนโปรเจกต์ที่ pin minimatch เก่าไว้โดยตั้งใจ:
npm audit# npm audit report
minimatch <=3.1.3Severity: highminimatch ReDoS vulnerability - https://github.com/advisories/GHSA-f8q6-p94x-37v3minimatch has a ReDoS via repeated wildcards with non-matching literal in pattern - https://github.com/advisories/GHSA-3ppc-4f35-3m26minimatch has ReDoS: matchOne() combinatorial backtracking via multiple non-adjacent GLOBSTAR segments - https://github.com/advisories/GHSA-7r86-cg39-jmmjminimatch ReDoS: nested *() extglobs generate catastrophically backtracking regular expressions - https://github.com/advisories/GHSA-23c5-xmqv-rm74fix available via `npm audit fix --force`Will install [email protected], which is outside the stated dependency rangenode_modules/minimatch
1 high severity vulnerabilitynpm audit fix apply fix ที่ยังอยู่ใน semver range เดิม; npm audit fix --force จะติดตั้ง version ที่นอกช่วงที่ประกาศไว้ (ตามที่ flag ไว้ข้างบน) เพื่อปิดช่องโหว่ ซึ่งอาจเป็น breaking change — ควร review เสมอก่อนรันมันแบบไม่มีคนดูใน CI
Workspaces
หัวข้อที่มีชื่อว่า “Workspaces”Workspaces ให้ root package.json ตัวเดียวจัดการหลาย package ใน repository เดียว (monorepo) โดย symlink cross-package dependency เข้าไปใน node_modules ของแต่ละ package แทนที่จะ publish แล้วติดตั้งใหม่ทุกครั้งที่มีการเปลี่ยนแปลง
{ "name": "my-monorepo", "private": true, "workspaces": ["packages/*"]}flowchart TD Root["root package.json (workspaces: packages/*)"] --> Core["packages/core (@ws-demo/core)"] Root --> Cli["packages/cli (@ws-demo/cli)"] Cli -->|"depends on ws-demo/core (workspace range: *)"| CoreLink["node_modules/@ws-demo/core -> symlink to packages/core"] CoreLink -.->|"resolves to"| Core
ผลลัพธ์จริงจาก workspace สอง package (packages/core, packages/cli ที่ cli depend อยู่บน core):
npm ls --allws-demo@ /path/to/ws-demo├─┬ @ws-demo/[email protected] -> ./packages/cli│ └── @ws-demo/[email protected] deduped -> ./packages/core└── @ws-demo/[email protected] -> ./packages/core-w/--workspace scope คำสั่งไปยัง package เดียวโดยไม่ต้อง cd เข้าไป:
npm -w @ws-demo/core exec -- node -e "console.log(require('./index.js').add(2,3))"5ไม่มี tarball ไม่มีขั้นตอน publish ไม่มี version bump — @ws-demo/cli เห็นสิ่งที่อยู่บน disk ใน packages/core ตอนนี้เลยผ่าน symlink ซึ่งเป็นสิ่งที่ทำให้ workspaces เร็วสำหรับการ iterate หลาย package ไปพร้อมกัน
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”ทุกคำสั่งจัดการ dependency ในบทนี้ตอบคำถามเดียวกัน: “byte ที่แน่ชัดที่ฉันกำลังจะรันคืออะไร และฉันเชื่อที่มาของมันได้ไหม” Lockfile ตอบคำถามนี้สำหรับการติดตั้งครั้งเดียว (integrity hash) npm ci ตอบสำหรับทั้ง environment (fail ถ้า lockfile กับ manifest ไม่ตรงกัน ล้าง node_modules ก่อนเสมอ) overrides ตอบเมื่อ transitive dependency ที่คุณไม่ได้ควบคุม ship สิ่งที่คุณต้องบังคับเปลี่ยนอยู่ดี npm audit ตอบเทียบกับ public vulnerability database ไม่มีอันไหนเกี่ยวกับ “ทำให้ dependency ทำงาน” — ทั้งหมดเกี่ยวกับการทำให้แน่ใจว่าสิ่งที่ติดตั้งจริงตรงกับสิ่งที่คุณเชื่อว่าติดตั้งไป
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”| ตัวเลือก | ประโยชน์ | ต้นทุน |
|---|---|---|
npm ci ใน CI/Docker | Reproducible, เร็ว (ไม่มีขั้นตอน resolve), fail ชัดเจนเมื่อ drift | ใช้เพิ่มหรืออัปเดต dependency ไม่ได้ — ยังต้อง npm install ในเครื่อง local ก่อน |
overrides สำหรับ transitive pin | แก้ dependency ที่มีช่องโหว่/พังที่ซ้อนอยู่โดยไม่ต้อง fork หรือรอ maintainer | ข้าม version ที่ maintainer เทสต์ไว้แล้ว — อาจสร้าง incompatibility ที่ override ไม่รู้ |
npm audit fix --force | ปิดช่องโหว่ที่ fix ปกติของ npm เข้าไม่ถึง | ติดตั้ง version นอกช่วงอัตโนมัติ — เป็นการเปลี่ยน behavior จริง ไม่ใช่แค่ patch |
| Workspaces | Iterate ในเครื่องได้เร็วข้าม package, ไม่ต้อง publish-แล้วติดตั้งใหม่วนไป | เพิ่มความซับซ้อนของ tooling monorepo; บั๊กที่เกิดเฉพาะใน workspace อาจมองไม่เห็นจนกว่า package จะถูก publish แยกกันจริง |
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “
npm ciแค่เป็นnpm installที่เร็วกว่า” มันเป็น contract ที่ต่างกัน: มัน refuse รันทั้งหมดถ้าpackage.jsonกับ lockfile ไม่ตรงกัน และมันลบnode_modulesก่อนเสมอ ความเร็วเป็นแค่ผลข้างเคียง ไม่ใช่จุดประสงค์ - “
overridesมีผลแค่กับ direct dependency ของคุณเองเท่านั้น” มันเข้าถึงทั้ง tree รวมถึง package ที่ซ้อนอยู่หลายชั้นที่คุณไม่เคยระบุในpackage.jsonของตัวเองเลย - “
npm audit fixกับnpm audit fix --forceทำสิ่งเดียวกัน แค่แรงกว่า” เวอร์ชันธรรมดา apply fix เฉพาะที่อยู่ใน semver range เดิมของคุณ--forceจะกระโดดไป version นอกช่วงที่ประกาศไว้ — เป็น upgrade จริง ที่มีโอกาส breaking change จริง
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”Dockerfile ที่ AI generate เอื้อมไปหา npm install บ่อยกว่า npm ci มาก เพราะ npm install คือคำสั่งที่ปรากฏในเกือบทุก tutorial และ README — npm ci เป็นความรู้เฉพาะ CI ที่โผล่ใน training data ทั่วไปน้อยกว่ามาก
🐛 หา bug ในโค้ดนี้
FROM node:24-slimWORKDIR /appCOPY package.json package-lock.json ./RUN npm installCOPY . .CMD ["node", "server.js"]เฉลย
npm install ภายใน Docker image build สามารถ resolve dependency tree ต่างจากที่บันทึกไว้ใน package-lock.json ได้ แม้จะมาจาก package.json เดียวกันเป๊ะ — lockfile ถูก copy เข้ามาแต่ไม่เคยถูก enforce build สองครั้งของ commit เดียวกัน ห่างกันหลายวัน สามารถ ship transitive version ต่างกันอย่างเงียบๆ วิธีแก้คือ RUN npm ci --omit=dev: มันติดตั้งตรงตามที่ lockfile ระบุ, fail การ build ถ้า lockfile ไม่ sync, และข้าม devDependencies ที่ไม่มีที่ทางใน production image