สวัสดี pnpm - ทางเลือกสำหรับ Node.js package manager
พอเราพูดถึง Package manager สำหรับ Node.js เรามักจะใช้ npm ซึ่งเป็นมาตรฐานโลกอยู่แล้ว ในบทความนี้เราจะมาดู pnpm ซึ่งเป็นหนึ่งในทางเลือกในการใช้งานแทน npm
npm มันไม่ดียังไง
Disk space
ปกติเวลาเรา download package ผ่าน npm
มันจะมาเก็บใน folder ชื่อ node_modules
ทีนี้ถ้าเราใช้ package version เดิมกับ 100 projects สิ่งที่เกิดขึ้นคือเราจะได้ copy ของ package มาอีก 100 อัน ซึ่งมันเปลืองที่เก็บเรานะ เช่นเดียวกัน ถ้าเรา update dependency version อันนึง นั่นหมายถึงเราต้อง clone dependency นั้นใหม่หมด
Dependency tree กับ speed
เริ่มจากเดิมที npm ก่อน version 3 จะมีหน้าตาของ dependency tree ซึ่งแต่ละ child dependency จะมี node_modules
เป็นของตัวเอง ประมาณนี้
node_modules
└─ foo
├─ index.js
├─ package.json
└─ node_modules
└─ bar
├─ index.js
└─ package.json
ปัญหาของ structure นี้คือมันจะมี package ตัวเดียวกันซึ่งถูกใช้หลายๆ อัน ซ้ำๆ กัน ซึ่งมันก็จะไปเกี่ยวกับ disk space อีกอันหนึ่งคือมันมีโอกาสที่จะเกิด nested package ยาวมากๆ ซึ่งจะทำให้ปัญหา path ยาว สำหรับ Windows ตามมา
เพื่อแก้ปัญหาข้างต้น npm version 3 เป็นต้นมาจึงเปลี่ยนเป็น dependency tree ใหม่ให้ package ถูก flat อยู่ใน level node_modules
ทั้งหมด เป็นหน้าตาแบบนี้
node_modules
├─ foo
| ├─ index.js
| └─ package.json
└─ bar
├─ index.js
└─ package.json
แต่มันก็มีปัญหาเล็กๆ ตามมาอีก เช่น การ flat dependency มันใช้เวลานาน และ source code เราสามารถ access package ที่ไม่ได้ใช้ในนั้นได้
ปล. Yarn ถึงแม้จะเร็วกว่า npm แต่ก็ใช้ approach นี้เหมือนกัน
pnpm แก้ปัญหานี้ยังไง
pnpm
ใช้ symlink (symbolic link คือการอ้างอิงถึง file หรือ directory ผ่าน path) สมมติว่าเรามี package foo
ซึ่งใช้ bar
ทั้งสองตัวจะถูกเก็บลงใน storage ของ pnpm
เองและใน foo
จะทำการ symlink ไปที่ bar
หลังจากนั้น foo
ก็จะถูก symlink โดย node_modules
บนสุดเหมือนกัน เพราะ foo
เป็น dependency ของ root project จะได้หน้าตาประมาณนี้
node_modules
├── foo -> ./.pnpm/foo@1.0.0/node_modules/foo
└── .pnpm
├── bar@1.0.0
│ └── node_modules
│ └── bar -> <store>/bar
└── foo@1.0.0
└── node_modules
├── foo -> <store>/foo
└── bar -> ../../bar@1.0.0/node_modules/bar
ดังนั้นเวลาเราสั่ง require('foo')
มันจะชี้ไปที่ node_modules/.pnpm/foo@1.0.0/node_modules/foo/index.js
นั่นเอง
แล้วสมมติเราเพิ่ม dependency ใหม่ชื่อ qar
ซึ่งถูกใช้โดย root, foo
และ bar
ก็จะได้หน้าตาประมาณนี้
node_modules
├── foo -> ./.pnpm/foo@1.0.0/node_modules/foo
└── .pnpm
├── bar@1.0.0
│ └── node_modules
│ ├── bar -> <store>/bar
│ └── qar -> ../../qar@2.0.0/node_modules/qar
├── foo@1.0.0
│ └── node_modules
│ ├── foo -> <store>/foo
│ ├── bar -> ../../bar@1.0.0/node_modules/bar
│ └── qar -> ../../qar@2.0.0/node_modules/qar
└── qar@2.0.0
└── node_modules
└── qar -> <store>/qar
ข้อสังเกต
- Data structure จะเปลี่ยนไปเป็น graph แทน เนื่องจากเราใช้การ symlink แทนนั่นเอง ทำให้การ access dependency เร็วขึ้น
- ประหยัด disk space เพราะการใช้ symlink ทำให้เราสามารถใช้ package เดียวอ้างอิงกับทั้ง project ได้เลย
ปิดท้ายด้วยข้อเสีย
หลักๆ เลยคือด้วยความที่ package สามารถ access ได้แค่ dependencies ที่ระบุไว้ใน package.json
เท่านั้น และความที่เป็น flatten node_modules
ทำให้การย้ายจาก npm
หรือ Yarn
มาที่ pnpm
อาจจะไม่ work 100% มีโอกาสสูง
โดยรวมแล้ว pnpm เป็น package manager ที่น่าใจอีกตัวหนึ่ง ที่มีจุดเด่นคือความเร็วและประหยัด disk space นอกจากนี้ยังสนับสนุนการจัด workspace แบบ monorepos ด้วย ไปลองกันได้ครับ