Monorepo & Astro
สิ่งที่จะสร้าง
หัวข้อที่มีชื่อว่า “สิ่งที่จะสร้าง”โครงกระดูกที่ทุกอย่างจะแขวนต่อจากนี้: monorepo แบบ pnpm-workspaces ที่มีสามแพ็กเกจ ซึ่งจะโตขึ้นเป็นทั้งแอป
offlinenotes/├── pnpm-workspace.yaml # packages: apps/*, crates/*├── package.json # root scripts (dev, build)├── apps/│ ├── web/ # Astro app (the PWA), TypeScript, Web Components│ └── sync/ # thin Hono sync server (placeholder for now)└── crates/ └── crdt/ # Rust CRDT, compiled to WASM (next lesson)พอจบบท คุณจะได้ apps/web รันเป็น Astro dev server บนพอร์ต 4321, apps/sync จองไว้สำหรับ Hono server (Module 9) และ crates/crdt จองไว้สำหรับ Rust engine (บทถัดไป) pnpm install ครั้งเดียวต่อสายทั้งสามตัวเข้าด้วยกัน
OfflineNotes จริง ๆ แล้วคือสามโปรแกรมที่ต้องเห็นตรงกันเรื่อง data format เดียว: browser app (TypeScript), sync server (TypeScript) และ CRDT engine (Rust) op ที่ออกจาก browser ต้องมีรูปร่างตรงเป๊ะกับที่ server เก็บและที่ engine merge การเก็บทั้งหมดไว้ใน repo เดียวหมายถึง clone ครั้งเดียว install ครั้งเดียว มีที่เดียวที่นิยาม type Op และไม่มี version-skew ระหว่างแพ็กเกจที่ publish แล้วกับแอปที่เรียกใช้
- pnpm workspaces ให้แต่ละแพ็กเกจถือ
package.jsonและ dependency ของตัวเองได้ ขณะที่ใช้ lockfile และ node_modules ร่วมกันตัวเดียว — ดังนั้นapps/webกับapps/syncต่างดึงของที่ตัวเองต้องการได้โดยไม่ต้องมี Hono สี่ก็อปปี้บนดิสก์ - Astro host
apps/webเพราะเราต้องการแอปแบบ static-first และ installable: Astro ส่ง JavaScript เป็นศูนย์โดยดีฟอลต์ bundle TypeScript และ Web Components ของเราด้วย Vite และภายหลังก็ build PWA (Service Worker + manifest) ได้สะอาด เราไม่ได้สร้าง server-rendered SPA — แอปรันทั้งหมดจาก IndexedDB ใน browser crates/อยู่ข้าง ๆapps/เพราะ CRDT เป็นส่วนหนึ่งของ build แบบ first-class ไม่ใช่ external dependencyapps/webcompile crate นี้ในเครื่องด้วยwasm-packแล้ว import output ออกมา
ข้อดีข้อเสีย
หัวข้อที่มีชื่อว่า “ข้อดีข้อเสีย”Monorepo (pnpm workspaces) เทียบกับ repo แยกสามตัว
- Pros: มี source of truth เดียวสำหรับ op format ที่ใช้ร่วมกัน; เปลี่ยนแปลงข้าม client, server และ engine แบบ atomic ใน commit เดียว; install ครั้งเดียว dev command เดียว
- Cons: toolchain ปนกัน (Node และ Rust ในต้นไม้เดียว); CI ต้อง build สอง ecosystem; repo ใหญ่ขึ้นเวลา clone
Astro เป็น host ของแอป เทียบกับ Vite SPA ล้วน ๆ
- Pros: output แบบ static ที่ Service Worker precache ไว้ได้ตรงตัว; islands/zero-JS โดยดีฟอลต์ทำให้ shell เล็ก; TypeScript และ Vite แบบ first-class โดยไม่ต้องเขียน config เอง
- Cons: component model ของ Astro (
.astrofiles, islands) เป็นอีกหนึ่งแนวคิดที่ต้องเรียน ทั้งที่ UI ตอนรันจริงของเราเป็น custom elements ล้วน; ฟีเจอร์บางอย่างของ Astro (SSR, view transitions) เราตั้งใจจะไม่ใช้
ติดตั้ง
หัวข้อที่มีชื่อว่า “ติดตั้ง”1. offlinenotes/pnpm-workspace.yaml
หัวข้อที่มีชื่อว่า “1. offlinenotes/pnpm-workspace.yaml”สร้างไดเรกทอรีโปรเจกต์แล้วประกาศ workspace glob pnpm จะถือทุกไดเรกทอรีที่ตรงกับ glob เหล่านี้และมี package.json เป็น workspace package
packages: - "apps/*" - "crates/*"crates/* ใส่ไว้เพื่อความสมมาตร; crates/crdt เป็น Rust crate ที่ไม่มี package.json ดังนั้น pnpm ก็แค่ข้ามไป — web app ใช้ WASM output ที่ compile แล้วเป็น build artifact ไม่ใช่ workspace dependency
2. offlinenotes/package.json
หัวข้อที่มีชื่อว่า “2. offlinenotes/package.json”root package เป็น private และถือ script ที่คุณจะรันทุกวัน เราจะเติมให้ครบเมื่อ WASM และ server พร้อมใช้งาน
{ "name": "offlinenotes", "private": true, "version": "0.0.0", "scripts": { "dev": "pnpm --filter web dev", "build": "pnpm --filter web build" }}--filter web เล็งไปที่แพ็กเกจที่ ชื่อ web (ที่เราตั้งในขั้นตอนที่ 3) ไม่ว่าจะอยู่โฟลเดอร์ไหน — การแยกส่วนแบบนี้คือเหตุผลที่เรา rename scaffold ด้านล่าง
3. apps/web — scaffold Astro + TypeScript
หัวข้อที่มีชื่อว่า “3. apps/web — scaffold Astro + TypeScript”สร้าง Astro app แบบ minimal ลงใน apps/web ตรง ๆ -- ส่ง flag ผ่าน pnpm ต่อไปยัง create-astro
pnpm create astro@latest apps/web -- \ --template minimal --typescript strict --no-install --no-gitจากนั้นเปิด apps/web/package.json แล้วตั้งชื่อเป็น web เพื่อให้ script --filter web ที่ root resolve ได้:
{ "name": "web", "type": "module", "version": "0.0.0", "scripts": { "dev": "astro dev", "build": "astro build", "preview": "astro preview" }}(scaffold เพิ่ม dependencies/devDependencies มาให้ — ปล่อยไว้ตามที่ generate มา มีแค่ name เท่านั้นที่สำคัญตรงนี้)
4. apps/sync/package.json — a placeholder
หัวข้อที่มีชื่อว่า “4. apps/sync/package.json — a placeholder”จองแพ็กเกจ sync server ไว้เพื่อให้ workspace ครบ Hono จะมาใน Module 9; ตอนนี้ยังเป็น private package เปล่า ๆ
{ "name": "sync", "private": true, "version": "0.0.0", "type": "module"}5. crates/crdt — reserve the folder
หัวข้อที่มีชื่อว่า “5. crates/crdt — reserve the folder”สร้างไดเรกทอรี crates/crdt/ เปล่า ๆ next lesson จะเติม Rust crate และ toolchain wasm-pack ลงไป ตอนนี้จึงยังไม่มีอะไรต้องเขียน — โฟลเดอร์แค่ต้องมีอยู่เพื่อให้ layout เข้าท่า
ตรวจสอบผล
หัวข้อที่มีชื่อว่า “ตรวจสอบผล”install จาก root ของ repo pnpm อ่าน pnpm-workspace.yaml resolve ทุกแพ็กเกจ แล้วสร้าง lockfile ตัวเดียว
pnpm installที่คาดว่าจะเห็น: pnpm รายงาน workspace package ที่เจอ แล้วเขียน pnpm-lock.yaml:
Scope: all 2 workspace projects...Done in Xsทีนี้เริ่ม web app ผ่าน root script:
pnpm devที่คาดว่าจะเห็น — dev server ของ Astro บนพอร์ตดีฟอลต์:
astro vX.X.X ready in NNN ms┃ Local http://localhost:4321/เปิด http://localhost:4321/ แล้วยืนยันว่าหน้า Astro ดีฟอลต์ render ออกมา นั่นคือ run check: workspace resolve ได้, --filter web เจอแอป และ Astro serve ได้ หยุด server ด้วย Ctrl+C
Check your understanding:
- ทำไม root
devscript ถึงใช้--filter webแทน--filter apps/web? อะไรที่แยกสองสิ่งนี้ออกจากกัน? crates/*อยู่ใน workspace glob แต่ pnpm ไม่เคย installcrates/crdtเลย ทำไมถึงไม่ install และapps/webได้ CRDT มาจริง ๆ อย่างไร?- Astro ให้อะไรกับเราที่ Vite SPA ล้วน ๆ ให้ไม่ได้ ทั้งที่สุดท้ายแล้ว UI ของเราเป็น vanilla custom elements?
- บอกต้นทุนที่เป็นรูปธรรมสักหนึ่งอย่างของการเอา Rust engine, browser app และ sync server มาไว้ใน repo เดียว
คุณมี monorepo แบบ pnpm-workspaces แล้ว: apps/web แบบ Astro + TypeScript ที่รันบนพอร์ต 4321, apps/sync แบบ placeholder และ crates/crdt เปล่า ๆ ที่รอ Rust install ครั้งเดียวเชื่อมทั้งหมดเข้าด้วยกัน; pnpm dev ครั้งเดียวรันแอป
ต่อไป เติม crates/crdt แล้วตั้ง toolchain ที่เปลี่ยน Rust ให้เป็น WASM module ที่ web app import ได้: The Rust Crate →