ใน EP.1 เราได้ปูพื้นฐานการสร้าง CLI, การติดตั้ง Dependencies ที่จำเป็น, และสร้างคำสั่งย่อย django
ที่สามารถรับ Input จากผู้ใช้ผ่าน inquirer
พร้อมแสดง Spinner จำลองจาก ora
กันไปแล้ว
สำหรับใน EP.2 นี้ เราจะมาทำในส่วนที่เป็นหัวใจหลักกันจริงๆ นั่นคือ การดึงโค้ดโปรเจกต์ต้นแบบ (Template) จาก GitHub Repository ที่เราเตรียมไว้ และปิดท้ายด้วยการ เพิ่มสีสันให้กับข้อความ เพื่อให้ CLI ของเราดูสวยงามและเป็นมิตรกับผู้ใช้มากขึ้นครับ
การทำคำสั่ง Generate Project
ก่อนที่เราจะเริ่มเขียนโค้ดกัน เราต้องมีโปรเจกต์ต้นแบบที่เก็บไว้บน GitHub ก่อน เพื่อให้ CLI ของเราไปดึงมาใช้งานได้ครับ
1. ขั้นตอนก่อนจะเริ่มเพิ่มโค้ดใน CLI
- ให้เข้าไปสร้าง Repository แบบ Public ในบัญชี GitHub ของคุณ (ในตัวอย่างจะใช้ชื่อว่า
Template-Django-Test
) - ให้สร้าง Branch เอาไว้ 2 Branch ชื่อ
original-version
และsecond-version
- ใน Branch
original-version
ให้สร้างไฟล์อะไรก็ได้ไว้เป็นไฟล์ตั้งต้น เช่นtest.txt
- ใน Branch
second-version
ให้สร้างโฟลเดอร์และไฟล์ภายในโฟล์เดอร์ที่สร้าง เช่นapp/test.txt
เมื่อเตรียม Repository เสร็จแล้ว ก็ถึงเวลามาแก้ไขโค้ดใน test.command.ts
ของเรากันครับ!
2. การสร้างฟังก์ชันตรวจสอบและจัดการ Path (Validators)
เพื่อให้ CLI ของเราทำงานได้อย่างมีประสิทธิภาพและป้องกันข้อผิดพลาดจาก User Input เราควรสร้างฟังก์ชันสำหรับตรวจสอบความถูกต้องของข้อมูล (Validation) และจัดการกับ Path ของไฟล์กันก่อนครับ
สร้างโฟลเดอร์ใหม่ชื่อ utils
ภายใน src
และสร้างไฟล์ validators.ts
ข้างใน (src/utils/validators.ts
) จากนั้นใส่โค้ดต่อไปนี้ลงไป:
// src/utils/validators.ts
import path from "path";
import klear from "kleur";
// ตรวจสอบชื่อโปรเจกต์
export const validateProjectName = (name: string): boolean | string => {
if (!name) return klear.red("Project name cannot be empty.");
if (!/^[a-zA-Z0-9_]+$/.test(name))
return klear.red(
"Project name can only contain letters, numbers, and underscores.",
);
return true;
};
// ตรวจสอบ Path ปลายทาง
export const validateDestinationPath = (input: string): boolean | string => {
if (!input) return true; // อนุญาตให้เว้นว่างได้
const resolvedPath = path.resolve(input);
if (!path.isAbsolute(resolvedPath)) {
return klear.red("Please provide an absolute path.");
}
return true;
};
// แปลง Path ที่รับเข้ามาให้เป็น Path เต็ม
export const resolvePath = (input: string): string => {
if (!input) return process.cwd(); // ถ้าเว้นว่าง ให้ใช้ Directory ปัจจุบัน
const resolvedPath = path.resolve(input);
return resolvedPath;
};
validateProjectName
: ฟังก์ชันนี้จะเช็กว่าชื่อโปรเจกต์ที่ผู้ใช้ป้อนเข้ามานั้นไม่เป็นค่าว่าง และต้องประกอบด้วยตัวอักษรภาษาอังกฤษ, ตัวเลข, หรือเครื่องหมาย_
เท่านั้นvalidateDestinationPath
: ใช้สำหรับตรวจสอบ Path ปลายทางที่ผู้ใช้กรอก โดยอนุญาตให้เว้นว่างได้ (ซึ่งหมายถึงการใช้โฟลเดอร์ปัจจุบัน) หากมีการกรอก จะต้องเป็น Absolute Path เท่านั้นresolvePath
: ทำหน้าที่แปลง Path ที่ผู้ใช้กรอก (ไม่ว่าจะเป็น Relative หรือ Absolute) ให้กลายเป็น Path แบบเต็ม (Absolute Path)
3. แก้ไข django.command.ts
เพื่อเชื่อมต่อกับ Git
ตอนนี้เราจะเปลี่ยนจากการจำลองการทำงานด้วย setTimeout
มาเป็นการใช้ degit
เพื่อดึงโปรเจกต์จริงๆ กันครับ
// src/commands/django.command.ts
import { Command } from "commander";
import consola from "consola";
import degit from "degit";
import klear from "kleur"; // เพิ่มเข้ามา
import inquirer from "inquirer";
import ora from "ora";
import {
resolvePath,
validateDestinationPath,
validateProjectName,
} from "../utils/validators"; // เพิ่มเข้ามา
// URL ของ Git Repo ที่เราสร้างไว้
const DJANGO_PUBLIC_GIT_REPO = "git@github.com:.../....git";
const PROJECT_TYPE_CHOICES = [
{
name: "Original Project",
value: "original",
},
{
name: "Second Project",
value: "second",
},
];
// ฟังก์ชันสำหรับเลือกว่าจะดึงโค้ดจาก Branch ไหน
const determineGit = (type: string): string => {
let repo = DJANGO_PUBLIC_GIT_REPO;
switch (type) {
case "original":
repo += "#original-version"; // #ตามด้วยชื่อ Branch
break;
case "second":
repo += "#second-version";
default:
repo = DJANGO_PUBLIC_GIT_REPO; // ถ้าไม่ตรงเลยให้ใช้ repo หลัก (main branch)
break;
}
return repo;
};
kleur
: เรา importkleur
เข้ามาเพื่อเตรียมใช้ในการเพิ่มสีสันให้กับข้อความvalidators
: เราจะ import ฟังก์ชันสำหรับตรวจสอบความถูกต้องของข้อมูลเข้ามา (แม้เราจะยังไม่ได้สร้างไฟล์ แต่เป็นการเตรียมโครงสร้างไว้)DJANGO_PUBLIC_GIT_REPO
: สร้างตัวแปรสำหรับเก็บ URL ของ Git repository เพื่อให้ง่ายต่อการแก้ไขในอนาคต (อย่าลืมเปลี่ยนเป็น Repo ของคุณเอง!)determineGit(type)
: คือฟังก์ชันที่เราสร้างขึ้นมาใหม่เพื่อสร้าง URL ที่ถูกต้องสำหรับdegit
โดยการนำชื่อ Branch มาต่อท้าย URL หลักในรูปแบบ#<branch_name>
4. อัปเกรด action
ให้ใช้งานได้จริง
ตอนนี้เราจะมาแก้ไขโค้ดในส่วนของ .action()
ให้ครบสมบูรณ์กันครับ โดยเพิ่มการรับ Input, การตรวจสอบความถูกต้อง ของ Input และเรียกใช้ degit
// src/commands/django.command.ts
// ... (ส่วนโค้ดด้านบนเหมือนเดิม) ...
export const DjangoCommand = new Command("django")
.alias("gdj")
.description("Generate Django project structure")
.action(async () => {
try {
// ใช้ klear เพื่อเพิ่มสีสันและความหนา
consola.info(
klear.bold().blue("Welcome to the Django Project Generator!"),
);
const args = await inquirer.prompt([
{
type: "list",
name: "type",
message: "Select project type:",
default: "original",
choices: PROJECT_TYPE_CHOICES,
},
{
type: "input",
name: "name",
message: "Enter the project name:",
default: "my_django_project",
validate: validateProjectName, // เพิ่มการตรวจสอบ
},
{
type: "input",
name: "destination",
message:
"Enter the destination path (leave empty for current directory):",
default: "",
validate: validateDestinationPath, // เพิ่มการตรวจสอบ
},
]);
const spinner = ora("Cloning project template...").start();
const { type, name, destination } = args;
// หา Path ที่จะติดตั้งโปรเจกต์
const resolvedDestination = destination
? resolvePath(destination)
: process.cwd();
const projectPath = `${resolvedDestination}/${name}`;
// เลือก Repo + Branch จาก Input ของผู้ใช้
const gitRepo = determineGit(type);
// สั่งให้ degit ทำงาน
const emitter = degit(gitRepo, {
cache: false,
force: true,
verbose: true,
});
await emitter.clone(projectPath);
spinner.stop(); // หยุด Spinner เมื่อทำงานเสร็จ
// แสดงผลลัพธ์สุดท้ายพร้อมสีสันสวยงาม ✨
consola.success(
klear
.bold()
.green(
`Project ${klear.blue(name)} created successfully at ${klear.blue(projectPath)}!`,
),
);
} catch (error) {
// จัดการ Error ที่อาจเกิดขึ้น
consola.error(
klear
.bold()
.red("An error occurred while generating the Django project:"),
error,
);
process.exit(1);
}
});
inquirer
:- เราเพิ่มค่า
destination
เพื่อให้ผู้ใช้กำหนดได้ว่าจะสร้างโปรเจกต์ไว้ที่โฟลเดอร์ไหน - เราเพิ่ม
validate
เข้าไปในคำถามname
และdestination
เพื่อเรียกใช้ฟังก์ชันตรวจสอบความถูกต้องของข้อมูลที่ผู้ใช้ป้อน (เช่น ชื่อโปรเจกต์ต้องไม่มีอักขระพิเศษ)
- เราเพิ่มค่า
- Path Resolution:
resolvedDestination
: หากผู้ใช้ป้อนdestination
มา เราจะใช้ path นั้น แต่ถ้าเว้นว่างไว้ จะใช้โฟลเดอร์ปัจจุบัน (process.cwd()
) เป็นที่ตั้งprojectPath
: คือ path เต็มๆ ที่จะใช้สร้างโปรเจกต์
- **
degit
:const emitter = degit(...)
: เป็นการสร้าง instance ของdegit
พร้อมระบุ repo และ option ต่างๆawait emitter.clone(projectPath)
: คือคำสั่งที่ให้degit
เริ่มทำการโคลนโปรเจกต์มายัง path ที่เรากำหนดไว้ ซึ่งเป็น asynchronous operation
- การใช้
kleur
**: เราได้นำkleur
มาใช้ตกแต่งข้อความที่แสดงผลผ่านconsola
ในส่วนต่างๆ เช่นklear.bold().blue(...)
: ทำให้ข้อความต้อนรับเป็นสีฟ้าตัวหนาklear.bold().green(...)
: แสดงข้อความเมื่อทำสำเร็จเป็นสีเขียวตัวหนาklear.bold().red(...)
: แสดงข้อความเมื่อเกิดข้อผิดพลาดเป็นสีแดงตัวหนา
บทสรุป
ในที่สุด CLI ของเราก็มีฟังก์ชันที่เป็นหัวใจสำคัญแล้วครับ ใน EP.2 นี้ เราได้เรียนรู้วิธีใช้ degit เพื่อดึงโปรเจกต์ต้นแบบจาก GitHub Repository ตาม Branch ที่ผู้ใช้เลือก พร้อมทั้งสร้างฟังก์ชัน validators เพื่อตรวจสอบข้อมูลจากผู้ใช้ให้ถูกต้องและป้องกันข้อผิดพลาดที่อาจเกิดขึ้น
นอกจากนี้ เรายังได้ใช้ kleur มาช่วยเพิ่มสีสันให้กับข้อความ ทำให้ CLI ของเราดูเป็นมืออาชีพ สวยงาม และสื่อสารกับผู้ใช้ได้ชัดเจนยิ่งขึ้น ไม่ว่าจะเป็นข้อความต้อนรับ, ข้อความแจ้งเตือนเมื่อสำเร็จ, หรือข้อความเมื่อเกิดข้อผิดพลาด
ใน EP. ถัดไป เราจะมาต่อยอด CLI ของเราให้สมบูรณ์ขึ้นไปอีกขั้น ด้วยการเพิ่มคำสั่งใหม่ๆ และเตรียมความพร้อมสำหรับการนำไปเผยแพร่ (Publish) เพื่อให้คนอื่นสามารถดาวน์โหลดไปใช้งานได้จริง อย่าลืมมาติดตามกันนะครับ