לדלג לתוכן

4.9 Node.js, npm וכלי פיתוח פתרון

פתרון - Node.js, npm וכלי פיתוח

פתרון תרגיל 1

א. calc.js:

const args = process.argv.slice(2);

if (args.length !== 3) {
    console.log("Usage: node calc.js <num1> <operator> <num2>");
    console.log("Operators: + - x /");
    process.exit(1);
}

const num1 = parseFloat(args[0]);
const operator = args[1];
const num2 = parseFloat(args[2]);

if (isNaN(num1) || isNaN(num2)) {
    console.log("Error: arguments must be numbers");
    process.exit(1);
}

let result;

switch (operator) {
    case "+":
        result = num1 + num2;
        break;
    case "-":
        result = num1 - num2;
        break;
    case "x":
        result = num1 * num2;
        break;
    case "/":
        if (num2 === 0) {
            console.log("Error: division by zero");
            process.exit(1);
        }
        result = num1 / num2;
        break;
    default:
        console.log(`Unknown operator: ${operator}`);
        process.exit(1);
}

console.log(parseFloat(result.toFixed(2)));

ב. fileinfo.js:

const fs = require("fs");
const path = require("path");

const filePath = process.argv[2];

if (!filePath) {
    console.log("Usage: node fileinfo.js <file-path>");
    process.exit(1);
}

try {
    const fullPath = path.resolve(filePath);
    const stats = fs.statSync(fullPath);
    const content = fs.readFileSync(fullPath, "utf-8");
    const lineCount = content.split("\n").length;

    console.log(`File: ${path.basename(fullPath)}`);
    console.log(`Size: ${stats.size} bytes`);
    console.log(`Created: ${stats.birthtime.toISOString().split("T")[0]}`);
    console.log(`Lines: ${lineCount}`);
} catch (error) {
    if (error.code === "ENOENT") {
        console.log(`Error: file not found - ${filePath}`);
    } else {
        console.log(`Error: ${error.message}`);
    }
    process.exit(1);
}

פתרון תרגיל 2

mkdir date-project
cd date-project
npm init -y
npm install dayjs chalk@4
npm i -D eslint prettier

index.js:

const dayjs = require("dayjs");
const chalk = require("chalk");

// current date
const now = dayjs();
console.log(chalk.blue.bold(`Today: ${now.format("dddd, MMMM D, YYYY")}`));
console.log(chalk.blue(`Time: ${now.format("HH:mm:ss")}`));

// days until new year
const nextYear = dayjs(`${now.year() + 1}-01-01`);
const daysUntil = nextYear.diff(now, "day");
console.log(chalk.green(`\n${daysUntil} days until New Year ${now.year() + 1}`));

// last 5 days of the week
console.log(chalk.yellow("\nUpcoming days:"));
for (let i = 1; i <= 5; i++) {
    const day = now.add(i, "day");
    console.log(chalk.yellow(`  ${day.format("ddd DD/MM")} - Day ${i}`));
}

.gitignore:

node_modules/

package.json scripts:

{
    "scripts": {
        "start": "node index.js",
        "lint": "eslint .",
        "format": "prettier --write ."
    }
}

פתרון תרגיל 3

אחרי npm create vite@latest todo-app -- --template vanilla:

index.html:

<!DOCTYPE html>
<html lang="he" dir="rtl">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Todo App</title>
    <link rel="stylesheet" href="/src/style.css">
</head>
<body>
    <div id="app">
        <h1 id="title"></h1>
        <form id="todoForm">
            <input type="text" id="todoInput" placeholder="Add a task..." required>
            <button type="submit">Add</button>
        </form>
        <ul id="todoList"></ul>
    </div>
    <script type="module" src="/src/main.js"></script>
</body>
</html>

src/main.js:

import "./style.css";

// use env variable
document.getElementById("title").textContent =
    import.meta.env.VITE_APP_TITLE || "Todo App";

// load todos from localStorage
let todos = JSON.parse(localStorage.getItem("todos") || "[]");

const form = document.getElementById("todoForm");
const input = document.getElementById("todoInput");
const list = document.getElementById("todoList");

function saveTodos() {
    localStorage.setItem("todos", JSON.stringify(todos));
}

function renderTodos() {
    list.innerHTML = "";

    todos.forEach((todo, index) => {
        const li = document.createElement("li");
        li.className = todo.completed ? "completed" : "";

        const checkbox = document.createElement("input");
        checkbox.type = "checkbox";
        checkbox.checked = todo.completed;
        checkbox.addEventListener("change", () => toggleTodo(index));

        const span = document.createElement("span");
        span.textContent = todo.text;

        const deleteBtn = document.createElement("button");
        deleteBtn.textContent = "Delete";
        deleteBtn.className = "delete-btn";
        deleteBtn.addEventListener("click", () => deleteTodo(index));

        li.appendChild(checkbox);
        li.appendChild(span);
        li.appendChild(deleteBtn);
        list.appendChild(li);
    });
}

function addTodo(text) {
    todos.push({ text, completed: false });
    saveTodos();
    renderTodos();
}

function toggleTodo(index) {
    todos[index].completed = !todos[index].completed;
    saveTodos();
    renderTodos();
}

function deleteTodo(index) {
    todos.splice(index, 1);
    saveTodos();
    renderTodos();
}

form.addEventListener("submit", (e) => {
    e.preventDefault();
    const text = input.value.trim();
    if (text) {
        addTodo(text);
        input.value = "";
    }
});

renderTodos();

src/style.css:

* {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}

body {
    font-family: Arial, sans-serif;
    max-width: 500px;
    margin: 40px auto;
    padding: 0 20px;
}

h1 {
    text-align: center;
    margin-bottom: 20px;
}

#todoForm {
    display: flex;
    gap: 8px;
    margin-bottom: 20px;
}

#todoInput {
    flex: 1;
    padding: 8px 12px;
    border: 1px solid #ccc;
    border-radius: 4px;
    font-size: 16px;
}

button {
    padding: 8px 16px;
    background: #4CAF50;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
}

ul {
    list-style: none;
}

li {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 10px;
    border-bottom: 1px solid #eee;
}

li.completed span {
    text-decoration: line-through;
    color: #999;
}

.delete-btn {
    margin-right: auto;
    background: #f44336;
    padding: 4px 8px;
    font-size: 12px;
}

.env:

VITE_APP_TITLE=My Todo App

פתרון תרגיל 4

npm i -D eslint prettier eslint-config-prettier

eslint.config.js:

import prettierConfig from "eslint-config-prettier";

export default [
    {
        rules: {
            "no-var": "error",
            "eqeqeq": "error",
            "no-unused-vars": "warn",
            "no-console": "off"
        }
    },
    prettierConfig
];

.prettierrc:

{
    "semi": true,
    "singleQuote": false,
    "tabWidth": 4,
    "printWidth": 100
}

package.json scripts:

{
    "scripts": {
        "dev": "vite",
        "build": "vite build",
        "preview": "vite preview",
        "lint": "eslint src/",
        "lint:fix": "eslint src/ --fix",
        "format": "prettier --write src/"
    }
}

קובץ עם שגיאות מכוונות (test-lint.js):

// ESLint should catch these:
var x = 5;          // no-var error
let unused = 10;    // no-unused-vars warning
if (x == "5") {     // eqeqeq error
    console.log("bad comparison");
}
npx eslint test-lint.js
# shows errors and warnings

פתרון תרגיל 5

npm install axios lodash uuid
# check package.json - all three appear in dependencies

npm uninstall axios
# axios removed from package.json and node_modules

rm -rf node_modules
npm install
# node_modules restored with lodash and uuid

index.js:

import { v4 as uuidv4 } from "uuid";
import _ from "lodash";

// generate 5 random IDs
const ids = _.times(5, () => uuidv4());
console.log("Random IDs:");
ids.forEach(id => console.log(`  ${id}`));

// lodash utilities
const data = [1, 2, 2, 3, 3, 3, 4];
console.log("\nOriginal:", data);
console.log("Unique:", _.uniq(data));
console.log("Chunk:", _.chunk(data, 3));
console.log("Sum:", _.sum(data));
console.log("Group by count:", _.countBy(data));
npm outdated
# shows table of outdated packages (if any)

פתרון תרגיל 6

vite.config.js:

import { defineConfig } from "vite";
import { resolve } from "path";

export default defineConfig({
    server: {
        port: 3000,
        open: true
    },
    resolve: {
        alias: {
            "@": resolve(__dirname, "src")
        }
    },
    build: {
        sourcemap: true
    }
});

src/modules/storage.js:

const STORAGE_KEY = "todos";

export function saveData(todos) {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
}

export function loadData() {
    const data = localStorage.getItem(STORAGE_KEY);
    return data ? JSON.parse(data) : [];
}

export function clearData() {
    localStorage.removeItem(STORAGE_KEY);
}

src/modules/utils.js:

export function generateId() {
    return Date.now().toString(36) + Math.random().toString(36).slice(2);
}

export function formatDate(date) {
    return new Date(date).toLocaleDateString("he-IL");
}

src/modules/ui.js:

export function createTodoElement(todo, onToggle, onDelete) {
    const li = document.createElement("li");
    li.className = todo.completed ? "completed" : "";

    const checkbox = document.createElement("input");
    checkbox.type = "checkbox";
    checkbox.checked = todo.completed;
    checkbox.addEventListener("change", () => onToggle(todo.id));

    const span = document.createElement("span");
    span.textContent = todo.text;

    const deleteBtn = document.createElement("button");
    deleteBtn.textContent = "Delete";
    deleteBtn.className = "delete-btn";
    deleteBtn.addEventListener("click", () => onDelete(todo.id));

    li.appendChild(checkbox);
    li.appendChild(span);
    li.appendChild(deleteBtn);

    return li;
}

export function showMessage(container, message) {
    container.textContent = message;
    setTimeout(() => {
        container.textContent = "";
    }, 3000);
}

src/main.js:

import "@/style.css";
import { saveData, loadData } from "@/modules/storage";
import { createTodoElement } from "@/modules/ui";
import { generateId } from "@/modules/utils";

const appTitle = import.meta.env.VITE_APP_TITLE || "Todo App";
const apiUrl = import.meta.env.VITE_API_URL || "http://localhost:3000";

console.log(`Running in ${import.meta.env.MODE} mode`);
console.log(`API URL: ${apiUrl}`);

document.getElementById("title").textContent = appTitle;

let todos = loadData();
const list = document.getElementById("todoList");
const form = document.getElementById("todoForm");
const input = document.getElementById("todoInput");

function render() {
    list.innerHTML = "";
    todos.forEach(todo => {
        const el = createTodoElement(todo, toggleTodo, deleteTodo);
        list.appendChild(el);
    });
}

function addTodo(text) {
    todos.push({ id: generateId(), text, completed: false });
    saveData(todos);
    render();
}

function toggleTodo(id) {
    todos = todos.map(t => t.id === id ? { ...t, completed: !t.completed } : t);
    saveData(todos);
    render();
}

function deleteTodo(id) {
    todos = todos.filter(t => t.id !== id);
    saveData(todos);
    render();
}

form.addEventListener("submit", (e) => {
    e.preventDefault();
    const text = input.value.trim();
    if (text) {
        addTodo(text);
        input.value = "";
    }
});

render();

.env.development:

VITE_APP_TITLE=Todo App (Dev)
VITE_API_URL=http://localhost:3001

.env.production:

VITE_APP_TITLE=Todo App
VITE_API_URL=https://api.production.com
npm run build
# check dist/ folder size and files
ls -la dist/assets/