Lesson 23-Node.js Advanced Application Development

Real-Time Application Development

WebSocket Real-Time Communication

WebSocket Server with ws Library:

// Create WebSocket server using ws library
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });

// Connection management
const clients = new Set();

wss.on('connection', (ws) => {
  clients.add(ws);
  console.log('New client connected, current connections:', clients.size);

  // Message handling
  ws.on('message', (message) => {
    console.log('Received message:', message);
    // Broadcast message to all clients
    clients.forEach(client => {
      if (client !== ws && client.readyState === WebSocket.OPEN) {
        client.send(message);
      }
    });
  });

  // Connection close
  ws.on('close', () => {
    clients.delete(ws);
    console.log('Client disconnected, remaining connections:', clients.size);
  });
});

console.log('WebSocket server started on port 8080');

Real-Time Communication with Socket.io:

// Socket.io server-side
const express = require('express');
const http = require('http');
const socketIo = require('socket.io');

const app = express();
const server = http.createServer(app);
const io = socketIo(server, {
  cors: {
    origin: "*",
    methods: ["GET", "POST"]
  }
});

// Room management
const rooms = {};

io.on('connection', (socket) => {
  console.log('New user connected:', socket.id);

  // Join room
  socket.on('joinRoom', (roomId) => {
    if (!rooms[roomId]) {
      rooms[roomId] = [];
    }
    rooms[roomId].push(socket.id);
    socket.join(roomId);
    console.log(`User ${socket.id} joined room ${roomId}`);
  });

  // Room message
  socket.on('roomMessage', ({ roomId, message }) => {
    io.to(roomId).emit('message', message);
  });

  // Disconnect
  socket.on('disconnect', () => {
    console.log('User disconnected:', socket.id);
    // Clean up room information
    for (const roomId in rooms) {
      rooms[roomId] = rooms[roomId].filter(id => id !== socket.id);
      if (rooms[roomId].length === 0) {
        delete rooms[roomId];
      }
    }
  });
});

server.listen(3000, () => {
  console.log('Server running at http://localhost:3000');
});

Message Queues

RabbitMQ Message Queue Implementation:

// Producer
const amqp = require('amqplib');

async function produce() {
  const connection = await amqp.connect('amqp://localhost');
  const channel = await connection.createChannel();

  const queue = 'task_queue';
  await channel.assertQueue(queue, { durable: true });

  const message = 'Hello RabbitMQ!';
  channel.sendToQueue(queue, Buffer.from(message), { persistent: true });
  console.log(` [x] Sent '${message}'`);

  setTimeout(() => {
    connection.close();
    process.exit(0);
  }, 500);
}

produce();

// Consumer
async function consume() {
  const connection = await amqp.connect('amqp://localhost');
  const channel = await connection.createChannel();

  const queue = 'task_queue';
  await channel.assertQueue(queue, { durable: true });

  channel.prefetch(1); // Fair dispatch
  console.log(' [*] Waiting for messages, press CTRL+C to exit');

  channel.consume(queue, (msg) => {
    if (msg !== null) {
      console.log(` [x] Received '${msg.content.toString()}'`);
      // Simulate work
      setTimeout(() => {
        console.log(' [x] Done');
        channel.ack(msg);
      }, 1000);
    }
  });
}

consume();

Kafka Message Queue Implementation:

// Producer
const { Kafka } = require('kafkajs');

const kafka = new Kafka({
  clientId: 'my-app',
  brokers: ['localhost:9092']
});

const producer = kafka.producer();

async function runProducer() {
  await producer.connect();
  await producer.send({
    topic: 'test-topic',
    messages: [
      { value: 'Hello Kafka!' }
    ]
  });
  await producer.disconnect();
}

runProducer();

// Consumer
const consumer = kafka.consumer({ groupId: 'test-group' });

async function runConsumer() {
  await consumer.connect();
  await consumer.subscribe({ topic: 'test-topic', fromBeginning: true });

  await consumer.run({
    eachMessage: async ({ topic, partition, message }) => {
      console.log({
        topic,
        partition,
        offset: message.offset,
        value: message.value.toString()
      });
    }
  });
}

runConsumer();

Real-Time Data Push

Server-Sent Events Implementation:

// SSE server-side
const express = require('express');
const app = express();

app.get('/events', (req, res) => {
  // Set SSE headers
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');

  // Send initial data
  res.write('event: connected\n');
  res.write('data: Welcome to SSE server!\n\n');

  // Send data periodically
  const intervalId = setInterval(() => {
    const data = { time: new Date().toISOString() };
    res.write(`data: ${JSON.stringify(data)}\n\n`);
  }, 1000);

  // Clean up on client disconnect
  req.on('close', () => {
    clearInterval(intervalId);
    console.log('Client disconnected');
  });
});

app.listen(3000, () => {
  console.log('SSE server running at http://localhost:3000/events');
});

// SSE client-side
const eventSource = new EventSource('http://localhost:3000/events');

eventSource.onmessage = (event) => {
  const data = JSON.parse(event.data);
  console.log('Received message:', data);
};

eventSource.addEventListener('connected', (event) => {
  console.log('Connection established:', event.data);
});

eventSource.onerror = (error) => {
  console.error('SSE error:', error);
};

Real-Time Collaborative Editing

Simple CRDT Implementation for Collaborative Editing:

// Simplified CRDT implementation (based on LWW-Element-Set)
class CRDTSet {
  constructor() {
    this.addSet = new Map(); // Set of add operations
    this.removeSet = new Map(); // Set of remove operations
    this.timestamp = 0; // Logical clock
  }

  add(element) {
    this.timestamp++;
    this.addSet.set(element, this.timestamp);
    this.removeSet.delete(element);
  }

  remove(element) {
    this.timestamp++;
    if (this.addSet.has(element)) {
      this.removeSet.set(element, this.timestamp);
    }
  }

  getElements() {
    const result = [];
    for (const [element, addTime] of this.addSet) {
      if (!this.removeSet.has(element) || this.removeSet.get(element) < addTime) {
        result.push(element);
      }
    }
    return result;
  }

  merge(other) {
    // Merge add set
    for (const [element, time] of other.addSet) {
      if (!this.addSet.has(element) || this.addSet.get(element) < time) {
        this.addSet.set(element, time);
      }
    }

    // Merge remove set
    for (const [element, time] of other.removeSet) {
      if (!this.removeSet.has(element) || this.removeSet.get(element) < time) {
        this.removeSet.set(element, time);
      }
    }

    // Update logical clock
    this.timestamp = Math.max(this.timestamp, other.timestamp);
  }
}

// Usage example
const crdt1 = new CRDTSet();
const crdt2 = new CRDTSet();

crdt1.add('Hello');
crdt2.add('World');
crdt1.merge(crdt2);

console.log(crdt1.getElements()); // ['Hello', 'World']

Simplified Operational Transformation (OT) Implementation:

// Simplified Operational Transformation (OT) implementation
class TextOperation {
  constructor() {
    this.ops = []; // Operation sequence
  }

  insert(position, text) {
    this.ops.push({ type: 'insert', position, text });
  }

  delete(position, length) {
    this.ops.push({ type: 'delete', position, length });
  }

  apply(text) {
    let result = text.split('');
    // Apply operations in reverse to maintain correct positions
    for (let i = this.ops.length - 1; i >= 0; i--) {
      const op = this.ops[i];
      if (op.type === 'insert') {
        result.splice(op.position, 0, op.text);
      } else if (op.type === 'delete') {
        result.splice(op.position, op.length);
      }
    }
    return result.join('');
  }

  transform(other) {
    const transformedOps = [];
    let thisIndex = 0;
    let otherIndex = 0;

    while (thisIndex < this.ops.length && otherIndex < other.ops.length) {
      const thisOp = this.ops[thisIndex];
      const otherOp = other.ops[otherIndex];

      if (thisOp.type === 'insert' && otherOp.type === 'insert') {
        if (thisOp.position <= otherOp.position) {
          transformedOps.push(thisOp);
          thisIndex++;
        } else {
          transformedOps.push(otherOp);
          otherIndex++;
        }
      } else if (thisOp.type === 'insert' && otherOp.type === 'delete') {
        if (thisOp.position <= otherOp.position) {
          transformedOps.push(thisOp);
          thisIndex++;
        } else {
          // Adjust delete position
          const adjustedOp = {
            type: 'delete',
            position: otherOp.position + thisOp.text.length,
            length: otherOp.length
          };
          transformedOps.push(adjustedOp);
          thisIndex++;
          otherIndex++;
        }
      }
      // Handle other cases...
    }

    // Add remaining operations
    while (thisIndex < this.ops.length) {
      transformedOps.push(this.ops[thisIndex++]);
    }

    while (otherIndex < other.ops.length) {
      transformedOps.push(other.ops[otherIndex++]);
    }

    return new TextOperation().fromArray(transformedOps);
  }

  fromArray(ops) {
    this.ops = ops;
    return this;
  }
}

// Usage example
const op1 = new TextOperation();
op1.insert(0, 'Hello');

const op2 = new TextOperation();
op2.insert(5, ' World');

// Transform op2 relative to op1
const transformedOp2 = op2.transform(op1);
console.log(transformedOp2.ops); // Should insert " World" at position 5

// Apply transformed operation
const text = '';
const result = transformedOp2.apply(text);
console.log(result); // 'Hello World'

Security and Permission Control in Real-Time Applications

Secure WebSocket Implementation:

// Secure WebSocket server with ws library
const WebSocket = require('ws');
const jwt = require('jsonwebtoken');

const wss = new WebSocket.Server({ port: 8080 });
const SECRET_KEY = 'your-secret-key';

// Connection authentication
wss.on('connection', (ws, req) => {
  // Extract token from query parameters
  const token = req.url.split('token=')[1];

  if (!token) {
    ws.close(1008, 'Authentication token required');
    return;
  }

  try {
    // Verify JWT
    const decoded = jwt.verify(token, SECRET_KEY);
    ws.user = decoded; // Store user information

    console.log('User authenticated:', decoded.username);

    // Message handling
    ws.on('message', (message) => {
      // Check permissions
      if (message.startsWith('admin:') && !decoded.roles.includes('admin')) {
        ws.send(JSON.stringify({ error: 'Permission denied' }));
        return;
      }

      // Process message...
      console.log('Received message:', message);
    });

    ws.on('close', () => {
      console.log('User disconnected:', decoded.username);
    });

  } catch (err) {
    ws.close(1008, 'Invalid token');
  }
});

console.log('Secure WebSocket server started on port 8080');

Socket.io Permission Control:

// Socket.io permission control implementation
const socketioJwt = require('socketio-jwt');

const io = socketIo(server, {
  cors: {
    origin: "*",
    methods: ["GET", "POST"]
  }
});

// JWT authentication middleware
io.use(socketioJwt.authorize({
  secret: 'your-secret-key',
  handshake: true
}));

// Protected namespace
const adminNamespace = io.of('/admin');
adminNamespace.use((socket, next) => {
  if (socket.decoded_token.roles.includes('admin')) {
    next();
  } else {
    next(new Error('Admin access required'));
  }
});

adminNamespace.on('connection', (socket) => {
  console.log('Admin connected:', socket.decoded_token.username);

  socket.on('manageUsers', (data) => {
    // Handle user management operations
    console.log('Admin operation:', data);
  });
});

// Regular namespace
io.on('connection', (socket) => {
  console.log('User connected:', socket.decoded_token.username);

  socket.on('sendMessage', (message) => {
    // Broadcast message to all users
    io.emit('newMessage', {
      user: socket.decoded_token.username,
      message
    });
  });
});

Microservices Architecture

Microservices Decomposition and Design Principles

Microservices Decomposition Example:

ecommerce-platform/
├── user-service/          # User service
├── product-service/       # Product service
├── order-service/         # Order service
├── payment-service/       # Payment service
└── api-gateway/           # API gateway

Microservices Design Principles:

  1. Single Responsibility Principle: Each service handles one business function
  2. Independent Deployment: Services can be deployed and scaled independently
  3. Lightweight Communication: Use REST or gRPC for communication
  4. Data Autonomy: Each service owns its database
  5. Fault Tolerance: Loose coupling between services to avoid cascading failures

Inter-Service Communication

RESTful API Communication:

// User service API
const express = require('express');
const app = express();
app.use(express.json());

let users = [
  { id: 1, name: 'John', email: 'john@example.com' }
];

// Get user
app.get('/users/:id', (req, res) => {
  const user = users.find(u => u.id === parseInt(req.params.id));
  if (!user) return res.status(404).send('User not found');
  res.json(user);
});

// Create user
app.post('/users', (req, res) => {
  const user = {
    id: users.length + 1,
    name: req.body.name,
    email: req.body.email
  };
  users.push(user);
  res.status(201).json(user);
});

app.listen(3000, () => {
  console.log('User service running at http://localhost:3000');
});

// Order service calling user service
const axios = require('axios');

async function getUser(userId) {
  try {
    const response = await axios.get(`http://localhost:3000/users/${userId}`);
    return response.data;
  } catch (error) {
    console.error('Failed to get user:', error.message);
    throw error;
  }
}

// Usage example
getUser(1).then(user => {
  console.log('User info:', user);
});

gRPC Communication Implementation:

// user.proto
syntax = "proto3";

service UserService {
  rpc GetUser (UserRequest) returns (UserResponse);
  rpc CreateUser (CreateUserRequest) returns (UserResponse);
}

message UserRequest {
  int32 id = 1;
}

message CreateUserRequest {
  string name = 1;
  string email = 2;
}

message UserResponse {
  int32 id = 1;
  string name = 2;
  string email = 3;
}
// User service (gRPC)
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');

const packageDefinition = protoLoader.loadSync('user.proto');
const userProto = grpc.loadPackageDefinition(packageDefinition).UserService;

const users = [
  { id: 1, name: 'John', email: 'john@example.com' }
];

function getUser(call, callback) {
  const user = users.find(u => u.id === call.request.id);
  if (!user) {
    callback({
      code: grpc.status.NOT_FOUND,
      message: 'User not found'
    });
    return;
  }
  callback(null, user);
}

function createUser(call, callback) {
  const user = {
    id: users.length + 1,
    name: call.request.name,
    email: call.request.email
  };
  users.push(user);
  callback(null, user);
}

const server = new grpc.Server();
server.addService(userProto.service, { getUser, createUser });
server.bindAsync(
  '0.0.0.0:50051',
  grpc.ServerCredentials.createInsecure(),
  (err, port) => {
    if (err) throw err;
    console.log(`gRPC server running on port ${port}`);
    server.start();
  }
);
// Order service (gRPC client)
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');

const packageDefinition = protoLoader.loadSync('user.proto');
const userProto = grpc.loadPackageDefinition(packageDefinition).UserService;

const client = new userProto.UserService(
  'localhost:50051',
  grpc.credentials.createInsecure()
);

function getUser(userId) {
  return new Promise((resolve, reject) => {
    client.getUser({ id: userId }, (err, response) => {
      if (err) {
        reject(err);
        return;
      }
      resolve(response);
    });
  });
}

// Usage example
getUser(1)
  .then(user => console.log('User info:', user))
  .catch(err => console.error('Failed to get user:', err));

Service Discovery and Registration

Consul Service Registration:

// Service registration with Consul
const consul = require('consul')();

const serviceName = 'user-service';
const serviceId = `user-service-${process.pid}`;
const servicePort = 3000;

// Register service
consul.agent.service.register({
  name: serviceName,
  id: serviceId,
  port: servicePort,
  check: {
    http: `http://localhost:${servicePort}/health`,
    interval: '10s',
    timeout: '5s'
  }
}, (err) => {
  if (err) throw err;
  console.log(`Service ${serviceName} registered with Consul`);
});

// Health check endpoint
const express = require('express');
const app = express();
app.get('/health', (req, res) => {
  res.status(200).send('OK');
});

app.listen(servicePort, () => {
  console.log(`Service running at http://localhost:${servicePort}`);
});

// Service discovery
function discoverService(serviceName) {
  consul.agent.service.list((err, services) => {
    if (err) throw err;

    const service = Object.values(services).find(s => s.Service === serviceName);
    if (!service) {
      throw new Error(`Service ${serviceName} not found`);
    }

    return `http://${service.Address}:${service.Port}`;
  });
}

// Usage example
discoverService('user-service').then(url => {
  console.log('User service address:', url);
});

Etcd Service Registration:

// Service registration with Etcd
const Etcd3 = require('etcd3');
const etcd = new Etcd3();

const serviceName = 'payment-service';
const serviceId = `payment-service-${process.pid}`;
const serviceKey = `/services/${serviceName}/${serviceId}`;
const serviceTtl = 10; // TTL in seconds

// Register service
async function registerService() {
  // Set initial TTL
  await etcd.put(serviceKey).value(JSON.stringify({
    name: serviceName,
    id: serviceId,
    port: 3001
  })).ttl(serviceTtl);

  // Periodically refresh TTL
  setInterval(async () => {
    try {
      await etcd.keepAlive(serviceKey);
      console.log(`Service ${serviceName} heartbeat maintained`);
    } catch (err) {
      console.error('Failed to maintain service heartbeat:', err);
    }
  }, serviceTtl * 1000 / 2);
}

registerService();

// Health check endpoint
const express = require('express');
const app = express();
app.get('/health', (req, res) => {
  res.status(200).send('OK');
});

app.listen(3001, () => {
  console.log(`Payment service running at http://localhost:3001`);
});

Distributed Transactions and Message Consistency

Saga Pattern Implementation:

// Saga coordinator implementation
class SagaCoordinator {
  constructor() {
    this.sagas = new Map();
  }

  async execute(sagaId, steps) {
    this.sagas.set(sagaId, { steps, current: 0, status: 'started' });

    try {
      for (let i = 0; i < steps.length; i++) {
        const step = steps[i];
        await this.executeStep(sagaId, step);
        this.sagas.get(sagaId).current = i + 1;
      }

      this.sagas.get(sagaId).status = 'completed';
      console.log(`Saga ${sagaId} completed`);
    } catch (error) {
      console.error(`Saga ${sagaId} failed:`, error.message);
      await this.compensate(sagaId);
      this.sagas.get(sagaId).status = 'failed';
    }
  }

  async executeStep(sagaId, step) {
    console.log(`Executing step ${step.name}`);
    // Call actual service
    // Simulate possible failure
    if (Math.random() < 0.2) {
      throw new Error(`Step ${step.name} failed`);
    }
    // Simulate delay
    await new Promise(resolve => setTimeout(resolve, 100));
  }

  async compensate(sagaId) {
    const saga = this.sagas.get(sagaId);
    // Roll back from current step
    for (let i = saga.current - 1; i >= 0; i--) {
      const step = saga.steps[i];
      console.log(`Compensating step ${step.name}`);
      // Call compensation operation
      // Simulate delay
      await new Promise(resolve => setTimeout(resolve, 50));
    }
  }
}

// Usage example
const coordinator = new SagaCoordinator();

const orderSaga = [
  { name: 'Create order', compensate: 'Cancel order' },
  { name: 'Deduct inventory', compensate: 'Restore inventory' },
  { name: 'Process payment', compensate: 'Refund payment' }
];

coordinator.execute('order-123', orderSaga);

Eventual Consistency with Message Queues:

// Achieve eventual consistency using message queues
const amqp = require('amqplib');

async function setupEventHandlers() {
  const connection = await amqp.connect('amqp://localhost');
  const channel = await connection.createChannel();

  const exchange = 'events';
  await channel.assertExchange(exchange, 'topic', { durable: true });

  // Order service - Publish events
  async function createOrder(order) {
    // 1. Save order to database
    console.log('Saving order:', order);

    // 2. Publish order created event
    channel.publish(
      exchange,
      'order.created',
      Buffer.from(JSON.stringify({
        type: 'order.created',
        data: order
      }))
    );

    console.log('Order created event published');
  }

  // Inventory service - Consume events
  await channel.assertQueue('inventory.queue', { durable: true });
  channel.bindQueue('inventory.queue', exchange, 'order.created');

  channel.consume('inventory.queue', async (msg) => {
    if (msg !== null) {
      const event = JSON.parse(msg.content.toString());
      if (event.type === 'order.created') {
        console.log('Processing inventory deduction:', event.data);
        // Inventory deduction logic...
      }
      channel.ack(msg);
    }
  });

  // Payment service - Consume events
  await channel.assertQueue('payment.queue', { durable: true });
  channel.bindQueue('payment.queue', exchange, 'order.created');

  channel.consume('payment.queue', async (msg) => {
    if (msg !== null) {
      const event = JSON.parse(msg.content.toString());
      if (event.type === 'order.created') {
        console.log('Processing payment:', event.data);
        // Payment logic...
      }
      channel.ack(msg);
    }
  });

  return { createOrder };
}

setupEventHandlers().then(({ createOrder }) => {
  // Simulate order creation
  createOrder({ id: 1, userId: 1, amount: 100 });
});

Microservices Monitoring and Logging

Prometheus Monitoring Implementation:

// Integrate Prometheus with Express application
const express = require('express');
const promBundle = require('express-prom-bundle');
const metricsMiddleware = promBundle({
  includeMethod: true,
  includePath: true,
  customLabels: { project_name: 'user_service' },
  promClient: { collectDefaultMetrics: {} }
});

const app = express();
app.use(metricsMiddleware);

// Business routes
app.get('/users', (req, res) => {
  // Simulate business processing
  setTimeout(() => {
    res.json([{ id: 1, name: 'John' }]);
  }, 100);
});

// Start server
app.listen(3000, () => {
  console.log('User service running at http://localhost:3000');
  console.log('Prometheus metrics endpoint: http://localhost:3000/metrics');
});

ELK Logging Integration:

// Integrate ELK with winston and winston-elasticsearch
const winston = require('winston');
require('winston-elasticsearch');

const esTransportOpts = {
  level: 'info',
  clientOpts: {
    node: 'http://localhost:9200',
    log: 'trace'
  }
};

const logger = winston.createLogger({
  transports: [
    new winston.transports.Console(),
    new winston.transports.File({ filename: 'combined.log' }),
    new winston.transports.Elasticsearch(esTransportOpts)
  ]
});

// Use logger in business code
logger.info('User service started', { service: 'user-service', pid: process.pid });
logger.error('Database connection failed', { error: 'Connection timeout' });

// Simulate business logs
setInterval(() => {
  logger.info('Processing user request', { 
    userId: Math.floor(Math.random() * 100),
    action: 'get_user'
  });
}, 5000);

Advanced CLI Tool Development

Interactive Command-Line Development

Interactive CLI with Inquirer.js:

const inquirer = require('inquirer');

async function promptUser() {
  const answers = await inquirer.prompt([
    {
      type: 'input',
      name: 'username',
      message: 'Enter username:',
      validate: input => input ? true : 'Username cannot be empty'
    },
    {
      type: 'password',
      name: 'password',
      message: 'Enter password:',
      mask: '*'
    },
    {
      type: 'list',
      name: 'role',
      message: 'Select role:',
      choices: ['Administrator', 'Developer', 'Tester'],
      filter: val => {
        const roles = { 'Administrator': 'admin', 'Developer': 'dev', 'Tester': 'test' };
        return roles[val];
      }
    },
    {
      type: 'checkbox',
      name: 'permissions',
      message: 'Select permissions:',
      choices: [
        { name: 'Read', value: 'read' },
        { name: 'Write', value: 'write' },
        { name: 'Delete', value: 'delete' }
      ]
    },
    {
      type: 'confirm',
      name: 'confirm',
      message: 'Confirm submission?',
      default: false
    }
  ]);

  console.log('User input:', answers);
}

promptUser();

Advanced Interactive Example:

const inquirer = require('inquirer');

async function setupProject() {
  // Step 1: Select project type
  const { projectType } = await inquirer.prompt([
    {
      type: 'list',
      name: 'projectType',
      message: 'Select project type:',
      choices: ['Web Application', 'API Service', 'Command-Line Tool']
    }
  ]);

  // Step 2: Dynamic questions based on project type
  let questions = [
    {
      type: 'input',
      name: 'projectName',
      message: 'Enter project name:',
      validate: input => /^[a-zA-Z0-9_-]+$/.test(input) 
        ? true 
        : 'Project name can only contain letters, numbers, underscores, and hyphens'
    }
  ];

  if (projectType === 'Web Application') {
    questions.push({
      type: 'checkbox',
      name: 'frameworks',
      message: 'Select frontend frameworks:',
      choices: ['React', 'Vue', 'Angular']
    });
  } else if (projectType === 'API Service') {
    questions.push({
      type: 'list',
      name: 'database',
      message: 'Select database:',
      choices: ['MongoDB', 'MySQL', 'PostgreSQL']
    });
  }

  const answers = await inquirer.prompt(questions);
  console.log('Project configuration:', { projectType, ...answers });

  // Step 3: Confirm creation
  const { confirm } = await inquirer.prompt([
    {
      type: 'confirm',
      name: 'confirm',
      message: 'Confirm project creation?',
      default: true
    }
  ]);

  if (confirm) {
    console.log('Starting project creation...');
    // Actual project creation logic...
  } else {
    console.log('Project creation cancelled');
  }
}

setupProject();

File Generation and Template Engines

Handlebars Template Engine:

const fs = require('fs');
const handlebars = require('handlebars');

// 1. Read template file
const templateSource = fs.readFileSync('template.hbs', 'utf8');

// 2. Compile template
const template = handlebars.compile(templateSource);

// 3. Prepare data
const data = {
  projectName: 'my-awesome-project',
  author: 'John Doe',
  year: new Date().getFullYear(),
  dependencies: ['express', 'lodash', 'axios']
};

// 4. Generate file content
const output = template(data);

// 5. Write to file
fs.writeFileSync('generated/README.md', output);
console.log('File generation completed');

EJS Template Engine:

const fs = require('fs');
const ejs = require('ejs');

// 1. Read template file
const templateSource = fs.readFileSync('template.ejs', 'utf8');

// 2. Render template
const output = ejs.render(templateSource, {
  projectName: 'my-awesome-cli',
  commands: [
    { name: 'init', description: 'Initialize project' },
    { name: 'build', description: 'Build project' },
    { name: 'deploy', description: 'Deploy project' }
  ]
});

// 3. Write to file
fs.writeFileSync('generated/HELP.md', output);
console.log('Help document generated');

Plugin System Design

Extensible CLI Architecture:

my-cli/
├── src/
│   ├── core/              # Core functionality
│   │   ├── cli.js         # CLI entry point
│   │   ├── pluginManager.js # Plugin manager
│   │   └── utils.js       # Utility functions
│   ├── plugins/           # Built-in plugins
│   │   ├── init/          # Init plugin
│   │   │   └── index.js
│   │   └── config/        # Config plugin
│   │       └── index.js
│   └── index.js           # Main entry point
└── package.json

Plugin Manager Implementation:

// pluginManager.js
class PluginManager {
  constructor() {
    this.plugins = new Map();
    this.hooks = new Map();
  }

  // Register plugin
  register(pluginName, plugin) {
    if (this.plugins.has(pluginName)) {
      throw new Error(`Plugin ${pluginName} already registered`);
    }

    this.plugins.set(pluginName, plugin);
    console.log(`Plugin ${pluginName} registered successfully`);

    // Execute plugin initialization hook
    if (typeof plugin.init === 'function') {
      plugin.init(this);
    }
  }

  // Execute command
  async executeCommand(command, args) {
    // Find matching plugin
    for (const [name, plugin] of this.plugins) {
      if (plugin.commands && plugin.commands[command]) {
        console.log(`Executing command ${command} from plugin ${name}`);
        return await plugin.commands[command](args);
      }
    }

    throw new Error(`Unknown command: ${command}`);
  }

  // Register hook
  registerHook(hookName, callback) {
    if (!this.hooks.has(hookName)) {
      this.hooks.set(hookName, []);
    }

    this.hooks.get(hookName).push(callback);
  }

  // Trigger hook
  async triggerHook(hookName, ...args) {
    if (this.hooks.has(hookName)) {
      for (const callback of this.hooks.get(hookName)) {
        await callback(...args);
      }
    }
  }
}

module.exports = PluginManager;

Built-in Plugin Example:

// plugins/init/index.js
module.exports = {
  name: 'init',

  commands: {
    init: async (args) => {
      console.log('Initializing project...');
      // Actual initialization logic...
      return { success: true };
    }
  },

  // Plugin lifecycle hook
  init(pluginManager) {
    // Register hooks
    pluginManager.registerHook('preInit', () => {
      console.log('init plugin: Preparing initialization');
    });

    pluginManager.registerHook('postInit', () => {
      console.log('init plugin: Initialization completed');
    });
  }
};

CLI Main Entry Point:

// src/index.js
const PluginManager = require('./core/pluginManager');
const path = require('path');

async function main() {
  const pluginManager = new PluginManager();

  // Load built-in plugins
  await loadBuiltinPlugins(pluginManager);

  // Load external plugins
  await loadExternalPlugins(pluginManager);

  // Parse command-line arguments
  const { command, args } = parseArgs(process.argv.slice(2));

  // Execute command
  try {
    await pluginManager.triggerHook('preCommand', command, args);
    const result = await pluginManager.executeCommand(command, args);
    await pluginManager.triggerHook('postCommand', command, args, result);

    if (result && result.success) {
      process.exit(0);
    } else {
      process.exit(1);
    }

  } catch (error) {
    console.error(`Command execution failed: ${error.message}`);
    await pluginManager.triggerHook('commandError', command, args, error);
    process.exit(1);
  }
}

// Load built-in plugins
async function loadBuiltinPlugins(pluginManager) {
  const builtinPlugins = [
    require('../plugins/init'),
    require('../plugins/config')
  ];

  for (const plugin of builtinPlugins) {
    pluginManager.register(plugin.name, plugin);
  }
}

// Load external plugins (from node_modules or specified directory)
async function loadExternalPlugins(pluginManager) {
  // Example: Load from globally installed plugins
  try {
    const myPlugin = require('my-cli-plugin');
    pluginManager.register('my-plugin', myPlugin);
    console.log('External plugin my-cli-plugin loaded successfully');
  } catch (err) {
    console.log('External plugin my-cli-plugin not found');
  }
}

// Simple command-line argument parsing
function parseArgs(args) {
  if (args.length === 0) {
    return { command: 'help', args: [] };
  }

  return {
    command: args[0],
    args: args.slice(1)
  };
}

// Start CLI
main();

Global and Local Installation Implementation

package.json Configuration:

{
  "name": "my-cli",
  "version": "1.0.0",
  "bin": {
    "mycli": "./bin/mycli.js"
  },
  "preferGlobal": true,
  "dependencies": {
    "chalk": "^4.1.2",
    "commander": "^9.4.1",
    "inquirer": "^8.2.4",
    "winston": "^3.8.2"
  },
  "devDependencies": {
    "eslint": "^8.30.0",
    "jest": "^29.3.1"
  }
}

Global Installation Script (bin/mycli.js):

#!/usr/bin/env node

const path = require('path');
const { program } = require('commander');
const chalk = require('chalk');
const pluginManager = require('../src/core/pluginManager');

// Set CLI basic information
program
  .name('mycli')
  .description('A powerful command-line tool')
  .version('1.0.0');

// Register built-in commands
program
  .command('init <project-name>')
  .description('Initialize new project')
  .action(async (projectName) => {
    console.log(chalk.blue(`Initializing project: ${projectName}`));
    // Actual initialization logic...
  });

program
  .command('config [key] [value]')
  .description('Manage configuration')
  .action((key, value) => {
    console.log(chalk.green(`Configuration operation: key=${key}, value=${value}`));
    // Actual configuration logic...
  });

// Plugin commands will be dynamically registered

// Parse command-line arguments
program.parse(process.argv);

// Show help if no command provided
if (process.argv.length < 3) {
  program.help();
}

// Dynamically load plugins and register commands
(async () => {
  // Initialize plugin manager
  const pluginManagerInstance = new PluginManager();

  // Load plugins (same as previous implementation)
  await loadBuiltinPlugins(pluginManagerInstance);
  await loadExternalPlugins(pluginManagerInstance);

  // Dynamically register plugin commands with Commander
  pluginManagerInstance.plugins.forEach((plugin, name) => {
    if (plugin.commands) {
      for (const [commandName, commandFn] of Object.entries(plugin.commands)) {
        program
          .command(commandName)
          .description(`Command provided by plugin ${name}`)
          .action(async (args) => {
            try {
              await commandFn(args);
            } catch (error) {
              console.error(chalk.red(`Error: ${error.message}`));
              process.exit(1);
            }
          });
      }
    }
  });
})();

Local Installation Usage Example:

// Use as local dependency in a project
const mycli = require('my-cli');

// Or run directly with npx
// npx mycli init my-project

CLI Tool Performance Optimization

Caching Mechanism Implementation:

const fs = require('fs');
const path = require('path');
const crypto = require('crypto');

class CacheManager {
  constructor(cacheDir = '.mycli-cache') {
    this.cacheDir = path.join(process.cwd(), cacheDir);
    this.ensureCacheDir();
  }

  ensureCacheDir() {
    if (!fs.existsSync(this.cacheDir)) {
      fs.mkdirSync(this.cacheDir, { recursive: true });
    }
  }

  getCacheKey(input) {
    return crypto.createHash('md5').update(JSON.stringify(input)).digest('hex');
  }

  async getFromCache(key) {
    const cacheFile = path.join(this.cacheDir, `${key}.json`);
    if (fs.existsSync(cacheFile)) {
      const data = fs.readFileSync(cacheFile, 'utf8');
      return JSON.parse(data);
    }
    return null;
  }

  async saveToCache(key, data) {
    const cacheFile = path.join(this.cacheDir, `${key}.json`);
    fs.writeFileSync(cacheFile, JSON.stringify(data), 'utf8');
  }

  async cachedOperation(key, operationFn) {
    // Try to get from cache
    const cachedData = await this.getFromCache(key);
    if (cachedData) {
      console.log('Reading data from cache');
      return cachedData;
    }

    // Perform actual operation
    console.log('Performing actual operation...');
    const result = await operationFn();

    // Save to cache
    await this.saveToCache(key, result);
    return result;
  }
}

// Usage example
(async () => {
  const cache = new CacheManager();

  const expensiveOperation = async () => {
    // Simulate expensive operation
    await new Promise(resolve => setTimeout(resolve, 2000));
    return { data: 'Result of expensive operation' };
  };

  const result = await cache.cachedOperation(
    'expensive-op-123', // Cache key
    expensiveOperation
  );

  console.log('Result:', result);
})();

Parallel Processing Optimization:

const { promisify } = require('util');
const exec = promisify(require('child_process').exec);
const pLimit = require('p-limit');

// Concurrency control example
async function processFiles(files) {
  // Limit concurrency to 4
  const limit = pLimit(4);

  const tasks = files.map(file => 
    limit(async () => {
      console.log(`Starting to process file: ${file}`);
      // Simulate file processing (e.g., code formatting, testing)
      await exec(`eslint --fix ${file}`);
      console.log(`Finished processing file: ${file}`);
    })
  );

  await Promise.all(tasks);
  console.log('All files processed');
}

// Usage example
processFiles(['file1.js', 'file2.js', 'file3.js', 'file4.js', 'file5.js']);

Incremental Build Optimization:

const fs = require('fs');
const path = require('path');
const crypto = require('crypto');

class IncrementalBuilder {
  constructor(srcDir, distDir) {
    this.srcDir = srcDir;
    this.distDir = distDir;
    this.manifest = this.loadManifest();
  }

  loadManifest() {
    const manifestFile = path.join(this.distDir, '.build-manifest.json');
    if (fs.existsSync(manifestFile)) {
      return JSON.parse(fs.readFileSync(manifestFile, 'utf8'));
    }
    return {};
  }

  saveManifest() {
    const manifestFile = path.join(this.distDir, '.build-manifest.json');
    fs.writeFileSync(manifestFile, JSON.stringify(this.manifest), 'utf8');
  }

  getFileHash(filePath) {
    const content = fs.readFileSync(filePath, 'utf8');
    return crypto.createHash('md5').update(content).digest('hex');
  }

  async buildFile(srcFile) {
    const relativePath = path.relative(this.srcDir, srcFile);
    const destFile = path.join(this.distDir, relativePath);

    // Ensure destination directory exists
    fs.mkdirSync(path.dirname(destFile), { recursive: true });

    // Calculate source file hash
    const currentHash = this.getFileHash(srcFile);

    // Check if rebuild is needed
    if (this.manifest[relativePath] === currentHash) {
      console.log(`Skipping unchanged file: ${relativePath}`);
      return;
    }

    // Perform build (simple file copy as example)
    fs.copyFileSync(srcFile, destFile);
    console.log(`Building file: ${relativePath}`);

    // Update manifest
    this.manifest[relativePath] = currentHash;
  }

  async build() {
    const files = this.getAllSourceFiles();
    for (const file of files) {
      await this.buildFile(file);
    }
    this.saveManifest();
    console.log('Build completed');
  }

  getAllSourceFiles() {
    const files = [];
    this.walkDir(this.srcDir, files);
    return files;
  }

  walkDir(dir, files) {
    const entries = fs.readdirSync(dir, { withFileTypes: true });
    for (const entry of entries) {
      const fullPath = path.join(dir, entry.name);
      if (entry.isDirectory()) {
        this.walkDir(fullPath, files);
      } else if (entry.isFile() && path.extname(entry.name) === '.js') {
        files.push(fullPath);
      }
    }
  }
}

// Usage example
(async () => {
  const builder = new IncrementalBuilder('./src', './dist');
  await builder.build();
})();

Performance Monitoring and Analysis:

const { performance, PerformanceObserver } = require('perf_hooks');

// Performance monitoring setup
function setupPerformanceMonitoring() {
  const obs = new PerformanceObserver((items) => {
    items.getEntries().forEach((entry) => {
      console.log(`${entry.name}: ${entry.duration.toFixed(2)}ms`);
    });
  });

  obs.observe({ entryTypes: ['measure'] });

  // Mark key stages
  performance.mark('start-cli');
}

// Record performance at key CLI points
function trackCliPerformance() {
  setupPerformanceMonitoring();

  // Simulate CLI startup process
  performance.mark('cli-init-start');
  // Initialization logic...
  performance.mark('cli-init-end');
  performance.measure('CLI Initialization', 'cli-init-start', 'cli-init-end');

  performance.mark('command-parse-start');
  // Command parsing logic...
  performance.mark('command-parse-end');
  performance.measure('Command Parsing', 'command-parse-start', 'command-parse-end');

  performance.mark('plugin-load-start');
  // Plugin loading logic...
  performance.mark('plugin-load-end');
  performance.measure('Plugin Loading', 'plugin-load-start', 'plugin-load-end');

  // End mark
  performance.mark('end-cli');
  performance.measure('Total Execution Time', 'start-cli', 'end-cli');
}

// Call in CLI main entry point
trackCliPerformance();

Through the above implementations, we have built a feature-rich, performance-optimized Node.js command-line tool framework that supports plugin extensions, caching mechanisms, parallel processing, and incremental builds. Developers can quickly create their own CLI tools based on this framework and extend functionality or optimize performance as needed.

Share your love