Skip to content

เริ่มต้นกับ Node.js

การติดตั้ง Node.js

  1. ดาวน์โหลดและติดตั้ง Node.js จาก nodejs.org faviconnodejs.org
  2. ตรวจสอบการติดตั้งด้วยคำสั่ง:
bash
node -v
npm -v

สร้าง CLI Application ด้วย Node.js

โครงสร้างโปรเจคพื้นฐาน

my-node-cli/
├── node_modules/
├── src/
│   ├── index.ts
│   ├── commands/
│   │   └── init.ts
│   └── utils/
│       └── logger.ts
├── dist/
├── package.json
├── tsconfig.json
└── package-lock.json

ขั้นตอนการสร้างโปรเจค

  1. สร้างโฟลเดอร์โปรเจค:
bash
mkdir my-node-cli
cd my-node-cli
  1. สร้างไฟล์ package.json:
bash
npm init -y
bash
pnpm init
bash
yarn init -y
bash
bun init
  1. ติดตั้ง dependencies:
bash
npm install @clack/prompts picocolors commander
npm install typescript @types/node tsx --save-dev
bash
pnpm add @clack/prompts picocolors commander
pnpm add -D typescript @types/node tsx
bash
yarn add @clack/prompts picocolors commander
yarn add -D typescript @types/node tsx
bash
bun add @clack/prompts picocolors commander
bun add -d typescript @types/node tsx
  1. สร้างไฟล์ tsconfig.json:
json
{
	"compilerOptions": {
		"target": "ES2020",
		"module": "NodeNext",
		"moduleResolution": "NodeNext",
		"esModuleInterop": true,
		"strict": true,
		"outDir": "./dist",
		"rootDir": "./src",
		"skipLibCheck": true,
		"resolveJsonModule": true
	},
	"include": ["src/**/*"],
	"exclude": ["node_modules", "dist"]
}
  1. แก้ไข package.json เพื่อเพิ่ม scripts และ bin:
json
{
	"name": "my-node-cli",
	"version": "1.0.0",
	"description": "Interactive CLI tool built with Clack and TypeScript",
	"type": "module",
	"bin": {
		"mycli": "./dist/index.js"
	},
	"scripts": {
		"dev": "tsx src/index.ts",
		"build": "tsc",
		"start": "node dist/index.js",
		"link": "npm link"
	},
	"keywords": ["cli", "typescript", "clack", "interactive"],
	"author": "",
	"license": "ISC"
}
  1. สร้างโฟลเดอร์และไฟล์ต่างๆ:
bash
mkdir -p src/commands src/utils
touch src/index.ts src/commands/init.ts src/utils/logger.ts
  1. เขียนโค้ดในไฟล์ต่างๆ:
ts
#!/usr/bin/env node
import * as p from "@clack/prompts";
import { Command } from "commander";
import pc from "picocolors";
import { initCommand } from "./commands/init.js";

// เริ่มต้นโปรแกรม
console.clear();
p.intro(`${pc.bgCyan(pc.black(" My CLI Tool "))}`);

const program = new Command();

program
  .name("mycli")
  .description("An awesome CLI tool built with Clack and TypeScript")
  .version("1.0.0");

program
  .command("init")
  .description("Initialize a new project")
  .option("-t, --template <template>", "specify project template", "default")
  .action(async (options) => {
    await initCommand(options);
  });

program
  .command("interactive")
  .description("Start interactive mode")
  .action(async () => {
    const projectType = await p.select({
      message: "What type of project do you want to create?",
      options: [
        { value: "web", label: "Web Application" },
        { value: "api", label: "API Service" },
        { value: "lib", label: "Library" },
      ],
    });

    if (p.isCancel(projectType)) {
      p.cancel("Operation cancelled");
      process.exit(0);
    }

    const projectName = await p.text({
      message: "What is your project name?",
      placeholder: "my-awesome-project",
      validate: (value) => {
        if (!value) return "Project name is required";
        if (!/^[a-z0-9-_]+$/.test(value)) {
          return "Project name can only contain lowercase letters, numbers, hyphens and underscores";
        }
        return undefined;
      },
    });

    if (p.isCancel(projectName)) {
      p.cancel("Operation cancelled");
      process.exit(0);
    }

    const features = await p.multiselect({
      message: "Select features to include",
      options: [
        { value: "typescript", label: "TypeScript", hint: "recommended" },
        { value: "eslint", label: "ESLint" },
        { value: "prettier", label: "Prettier" },
        { value: "jest", label: "Jest" },
      ],
      required: false,
    });

    if (p.isCancel(features)) {
      p.cancel("Operation cancelled");
      process.exit(0);
    }

    const spinner = p.spinner();
    spinner.start("Creating project...");

    // Simulate project creation
    await new Promise((resolve) => setTimeout(resolve, 2000));

    spinner.stop("Project created successfully!");

    p.note(
      `
      ${pc.green("✓")} Project type: ${pc.cyan(projectType as string)}
      ${pc.green("✓")} Project name: ${pc.cyan(projectName)}
      ${pc.green("✓")} Features: ${
        pc.cyan((features as string[]).join(", ") || "none")
      }
      
      ${pc.yellow("Next steps:")}
      ${pc.dim("$")} cd ${projectName}
      ${pc.dim("$")} npm install
      ${pc.dim("$")} npm run dev
    `,
      "Project summary",
    );

    p.outro(
      `Problems? ${
        pc.underline("https://github.com/yourusername/my-node-cli/issues")
      }`,
    );
  });

program.parse();

// If no args provided, show help
if (process.argv.length <= 2) {
  program.help();
}
ts
import * as p from "@clack/prompts";
import fs from "fs/promises";
import path from "path";
import pc from "picocolors";
import { fileURLToPath } from "url";

// Convert ESM __filename and __dirname
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

export async function initCommand(options: { template: string }) {
  const projectName = await p.text({
    message: "What is your project name?",
    placeholder: "my-awesome-project",
    validate: (value) => {
      if (!value) return "Project name is required";
      if (!/^[a-z0-9-_]+$/.test(value)) {
        return "Project name can only contain lowercase letters, numbers, hyphens and underscores";
      }
      return undefined;
    },
  });

  if (p.isCancel(projectName)) {
    p.cancel("Operation cancelled");
    process.exit(0);
  }

  const confirm = await p.confirm({
    message:
      `Create project "${projectName}" with template "${options.template}"?`,
  });

  if (!confirm || p.isCancel(confirm)) {
    p.cancel("Operation cancelled");
    process.exit(0);
  }

  const spinner = p.spinner();
  spinner.start("Creating project directory...");

  try {
    // Create project directory
    await fs.mkdir(projectName, { recursive: true });

    // Wait a bit to simulate work
    await new Promise((resolve) => setTimeout(resolve, 1000));

    spinner.message("Generating project files...");

    // Create basic files
    const packageJson = {
      name: projectName,
      version: "0.1.0",
      description: "Project created with my-node-cli",
      type: "module",
      scripts: {
        dev: "node index.js",
        test: "echo \"Error: no test specified\" && exit 1",
      },
      keywords: [],
      author: "",
      license: "ISC",
    };

    await fs.writeFile(
      path.join(projectName, "package.json"),
      JSON.stringify(packageJson, null, 2),
    );

    await fs.writeFile(
      path.join(projectName, "index.js"),
      `console.log('Hello from ${projectName}!');\n`,
    );

    await fs.writeFile(
      path.join(projectName, "README.md"),
      `# ${projectName}\n\nProject created with my-node-cli using the ${options.template} template.\n`,
    );

    // Wait a bit more to simulate work
    await new Promise((resolve) => setTimeout(resolve, 1000));

    spinner.stop("Project created successfully!");

    p.note(
      `
      ${pc.green("✓")} Project created at ${pc.cyan(`./${projectName}`)}
      
      ${pc.yellow("Next steps:")}
      ${pc.dim("$")} cd ${projectName}
      ${pc.dim("$")} npm install
      ${pc.dim("$")} npm run dev
    `,
      "Success!",
    );
  } catch (error) {
    spinner.stop("Failed to create project");
    p.log.error(`Error: ${(error as Error).message}`);
    process.exit(1);
  }
}
ts
import pc from "picocolors";

export const logger = {
  info: (message: string) => console.log(pc.blue(`ℹ ${message}`)),
  success: (message: string) => console.log(pc.green(`✓ ${message}`)),
  warning: (message: string) => console.log(pc.yellow(`⚠ ${message}`)),
  error: (message: string) => console.log(pc.red(`✗ ${message}`)),
  highlight: (message: string) => pc.cyan(message),
  dim: (message: string) => pc.dim(message),
};
  1. ทำให้ CLI สามารถรันได้ทั่วโลก (global):
bash
# สร้าง build ก่อน
npm run build

# ลิงก์ให้ใช้งานได้ทั่วโลก
npm link
bash
# สร้าง build ก่อน
pnpm build

# ลิงก์ให้ใช้งานได้ทั่วโลก
pnpm link --global
bash
# สร้าง build ก่อน
yarn build

# ลิงก์ให้ใช้งานได้ทั่วโลก
yarn link
bash
# สร้าง build ก่อน
bun run build

# ลิงก์ให้ใช้งานได้ทั่วโลก
bun link

การใช้งาน CLI

หลังจากลิงก์แล้ว คุณสามารถใช้ CLI ได้ทั่วไปในระบบ:

bash
# แสดงวิธีใช้และคำสั่งทั้งหมด
mycli --help

# สร้างโปรเจคใหม่ด้วยคำสั่ง init
mycli init

# ใช้โหมด interactive
mycli interactive

ตัวอย่าง UI ของ CLI

เมื่อรัน mycli interactive คุณจะเห็นอินเตอร์เฟซแบบโต้ตอบดังนี้:

┌ My CLI Tool ┐
└─────────────┘

◆  What type of project do you want to create?
│  Web Application
│  API Service
│  Library

◆  What is your project name?
│  my-awesome-project

◆  Select features to include
│  ✓ TypeScript (recommended)
│  ✓ ESLint
│    Prettier
│  ✓ Jest

◎  Creating project...
✓  Project created successfully!

▶  Project summary
   ✓ Project type: web
   ✓ Project name: my-awesome-project
   ✓ Features: typescript, eslint, jest
   
   Next steps:
   $ cd my-awesome-project
   $ npm install
   $ npm run dev

◆  Problems? https://github.com/yourusername/my-node-cli/issues

การเพิ่มคำสั่งใหม่

หากต้องการเพิ่มคำสั่งใหม่ในโปรเจค สามารถทำได้โดยสร้างไฟล์ในโฟลเดอร์ commands และเพิ่มคำสั่งใน index.ts:

ts
// src/commands/generate.ts
import * as p from "@clack/prompts";
import pc from "picocolors";

export async function generateCommand(type: string) {
  p.intro(`${pc.bgMagenta(pc.black(" Generate Command "))}`);

  // ตรรกะสำหรับการสร้างไฟล์ตามประเภท
  p.log.success(`Generated ${pc.cyan(type)} successfully!`);

  p.outro("Done!");
}

จากนั้นเพิ่มคำสั่งในไฟล์ index.ts:

ts
program
  .command("generate <type>")
  .description("Generate a new file (component, model, etc.)")
  .action(async (type) => {
    await generateCommand(type);
  });

การจัดการข้อผิดพลาด

เพิ่มการจัดการข้อผิดพลาดในแอปพลิเคชัน CLI ของคุณ: