Files
2025-11-29 18:20:21 +08:00

20 KiB

Legacy Code Modernization Operation

Update legacy code patterns to modern JavaScript/TypeScript standards and best practices.

Parameters

Received from $ARGUMENTS: All arguments after "modernize"

Expected format:

scope:"<path>" targets:"<target1,target2>" [compatibility:"<version>"]

Parameter definitions:

  • scope (REQUIRED): Path to modernize (e.g., "src/legacy/", "utils/old-helpers.js")
  • targets (REQUIRED): Comma-separated modernization targets
    • callbacks-to-async - Convert callbacks to async/await
    • var-to-const - Replace var with const/let
    • prototypes-to-classes - Convert prototypes to ES6 classes
    • commonjs-to-esm - Convert CommonJS to ES modules
    • jquery-to-vanilla - Replace jQuery with vanilla JS
    • classes-to-hooks - Convert React class components to hooks
    • legacy-api - Update deprecated API usage
  • compatibility (OPTIONAL): Target environment (e.g., "node14+", "es2020", "modern-browsers")

Workflow

1. Analyze Legacy Patterns

Identify legacy code to modernize:

# Find var usage
grep -r "var " <scope> --include="*.js" --include="*.ts"

# Find callback patterns
grep -r "function.*callback" <scope>

# Find prototype usage
grep -r ".prototype" <scope>

# Find require() usage
grep -r "require\(" <scope>

# Find jQuery usage
grep -r "\$\(" <scope>

2. Target-Specific Modernization

Modernization Examples

Target 1: Callbacks to Async/Await

When to modernize:

  • Callback hell (deeply nested callbacks)
  • Error handling is scattered
  • Readability suffers
  • Modern runtime supports async/await

Before (Callback hell):

// database.js
function getUser(userId, callback) {
  db.query('SELECT * FROM users WHERE id = ?', [userId], function(err, user) {
    if (err) {
      return callback(err);
    }

    db.query('SELECT * FROM posts WHERE author_id = ?', [userId], function(err, posts) {
      if (err) {
        return callback(err);
      }

      db.query('SELECT * FROM comments WHERE user_id = ?', [userId], function(err, comments) {
        if (err) {
          return callback(err);
        }

        callback(null, {
          user: user,
          posts: posts,
          comments: comments
        });
      });
    });
  });
}

// Usage
getUser(123, function(err, data) {
  if (err) {
    console.error('Error:', err);
    return;
  }

  console.log('User:', data.user);
  console.log('Posts:', data.posts);
  console.log('Comments:', data.comments);
});

After (Async/await - Clean and readable):

// database.ts
import { query } from './db';

interface User {
  id: number;
  name: string;
  email: string;
}

interface Post {
  id: number;
  title: string;
  content: string;
  authorId: number;
}

interface Comment {
  id: number;
  content: string;
  userId: number;
}

interface UserWithContent {
  user: User;
  posts: Post[];
  comments: Comment[];
}

async function getUser(userId: number): Promise<UserWithContent> {
  // Parallel execution for better performance
  const [user, posts, comments] = await Promise.all([
    query<User>('SELECT * FROM users WHERE id = ?', [userId]),
    query<Post[]>('SELECT * FROM posts WHERE author_id = ?', [userId]),
    query<Comment[]>('SELECT * FROM comments WHERE user_id = ?', [userId])
  ]);

  return { user, posts, comments };
}

// Usage - Much cleaner
try {
  const data = await getUser(123);
  console.log('User:', data.user);
  console.log('Posts:', data.posts);
  console.log('Comments:', data.comments);
} catch (error) {
  console.error('Error:', error);
}

More callback conversions:

// Before: fs callbacks
const fs = require('fs');

fs.readFile('config.json', 'utf8', function(err, data) {
  if (err) {
    console.error(err);
    return;
  }

  const config = JSON.parse(data);
  fs.writeFile('output.json', JSON.stringify(config), function(err) {
    if (err) {
      console.error(err);
      return;
    }
    console.log('Done');
  });
});

// After: fs promises
import { readFile, writeFile } from 'fs/promises';

try {
  const data = await readFile('config.json', 'utf8');
  const config = JSON.parse(data);
  await writeFile('output.json', JSON.stringify(config));
  console.log('Done');
} catch (error) {
  console.error(error);
}

Improvements:

  • No callback hell: Flat, linear code
  • Better error handling: Single try/catch
  • Parallel execution: Promise.all() for performance
  • Type safety: Full TypeScript support
  • Readability: Much easier to understand

Target 2: var to const/let

When to modernize:

  • Using old var declarations
  • Want block scoping
  • Prevent accidental reassignment
  • Modern ES6+ environment

Before (var - function scoped, hoisted):

function processOrders() {
  var total = 0;
  var count = 0;

  for (var i = 0; i < orders.length; i++) {
    var order = orders[i];
    var price = order.price;
    var quantity = order.quantity;

    total += price * quantity;
    count++;
  }

  // i is still accessible here (function scoped!)
  console.log(i); // orders.length

  return { total: total, count: count };
}

// Hoisting issues
function example() {
  console.log(x); // undefined (not error)
  var x = 10;
}

// Loop issues
for (var i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i); // Always prints 3!
  }, 100);
}

After (const/let - block scoped, not hoisted):

function processOrders(): { total: number; count: number } {
  let total = 0;
  let count = 0;

  for (let i = 0; i < orders.length; i++) {
    const order = orders[i];
    const price = order.price;
    const quantity = order.quantity;

    total += price * quantity;
    count++;
  }

  // i is NOT accessible here (block scoped)
  // console.log(i); // Error: i is not defined

  return { total, count };
}

// No hoisting issues
function example() {
  console.log(x); // Error: Cannot access 'x' before initialization
  const x = 10;
}

// Loop fixed
for (let i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i); // Prints 0, 1, 2 correctly
  }, 100);
}

Guidelines:

  • Use const by default (immutable binding)
  • Use let when reassignment needed
  • Never use var in modern code
  • Block scope prevents many bugs

Target 3: Prototypes to ES6 Classes

When to modernize:

  • Using prototype-based inheritance
  • Want cleaner OOP syntax
  • Better IDE support needed
  • Modern JavaScript environment

Before (Prototype pattern):

// Animal.js
function Animal(name, age) {
  this.name = name;
  this.age = age;
}

Animal.prototype.speak = function() {
  console.log(this.name + ' makes a sound');
};

Animal.prototype.getInfo = function() {
  return this.name + ' is ' + this.age + ' years old';
};

// Dog.js
function Dog(name, age, breed) {
  Animal.call(this, name, age);
  this.breed = breed;
}

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.speak = function() {
  console.log(this.name + ' barks');
};

Dog.prototype.fetch = function() {
  console.log(this.name + ' fetches the ball');
};

// Usage
var dog = new Dog('Rex', 3, 'Labrador');
dog.speak(); // Rex barks
console.log(dog.getInfo()); // Rex is 3 years old

After (ES6 Classes):

// Animal.ts
export class Animal {
  constructor(
    protected name: string,
    protected age: number
  ) {}

  speak(): void {
    console.log(`${this.name} makes a sound`);
  }

  getInfo(): string {
    return `${this.name} is ${this.age} years old`;
  }
}

// Dog.ts
export class Dog extends Animal {
  constructor(
    name: string,
    age: number,
    private breed: string
  ) {
    super(name, age);
  }

  speak(): void {
    console.log(`${this.name} barks`);
  }

  fetch(): void {
    console.log(`${this.name} fetches the ball`);
  }

  getBreed(): string {
    return this.breed;
  }
}

// Usage
const dog = new Dog('Rex', 3, 'Labrador');
dog.speak(); // Rex barks
console.log(dog.getInfo()); // Rex is 3 years old
console.log(dog.getBreed()); // Labrador

Improvements:

  • Cleaner syntax: More readable
  • Better inheritance: extends keyword
  • Access modifiers: public, private, protected
  • Type safety: Full TypeScript support
  • IDE support: Better autocomplete

Target 4: CommonJS to ES Modules

When to modernize:

  • Using require() and module.exports
  • Want tree-shaking benefits
  • Modern bundler support
  • Better static analysis

Before (CommonJS):

// utils.js
const crypto = require('crypto');
const fs = require('fs');

function generateId() {
  return crypto.randomUUID();
}

function readConfig() {
  return JSON.parse(fs.readFileSync('config.json', 'utf8'));
}

module.exports = {
  generateId,
  readConfig
};

// user-service.js
const { generateId } = require('./utils');
const db = require('./database');

class UserService {
  async createUser(data) {
    const id = generateId();
    return db.users.create({ ...data, id });
  }
}

module.exports = UserService;

// index.js
const express = require('express');
const UserService = require('./user-service');

const app = express();
const userService = new UserService();

app.post('/users', async (req, res) => {
  const user = await userService.createUser(req.body);
  res.json(user);
});

module.exports = app;

After (ES Modules):

// utils.ts
import { randomUUID } from 'crypto';
import { readFileSync } from 'fs';

export function generateId(): string {
  return randomUUID();
}

export function readConfig(): Config {
  return JSON.parse(readFileSync('config.json', 'utf8'));
}

// user-service.ts
import { generateId } from './utils.js';
import { db } from './database.js';

export class UserService {
  async createUser(data: CreateUserInput): Promise<User> {
    const id = generateId();
    return db.users.create({ ...data, id });
  }
}

// index.ts
import express from 'express';
import { UserService } from './user-service.js';

const app = express();
const userService = new UserService();

app.post('/users', async (req, res) => {
  const user = await userService.createUser(req.body);
  res.json(user);
});

export default app;

Improvements:

  • Tree-shaking: Remove unused exports
  • Static imports: Better bundler optimization
  • Named exports: More explicit imports
  • Top-level await: Possible in ES modules
  • Standard: Modern JavaScript standard

Target 5: jQuery to Vanilla JavaScript

When to modernize:

  • Remove jQuery dependency
  • Reduce bundle size
  • Modern browsers support native APIs
  • Better performance

Before (jQuery):

// app.js - Heavy jQuery usage
$(document).ready(function() {
  // DOM selection
  var $button = $('#submit-button');
  var $form = $('.user-form');
  var $inputs = $form.find('input');

  // Event handling
  $button.on('click', function(e) {
    e.preventDefault();

    // Get form data
    var formData = {};
    $inputs.each(function() {
      var $input = $(this);
      formData[$input.attr('name')] = $input.val();
    });

    // AJAX request
    $.ajax({
      url: '/api/users',
      method: 'POST',
      data: JSON.stringify(formData),
      contentType: 'application/json',
      success: function(response) {
        // DOM manipulation
        var $message = $('<div>')
          .addClass('success-message')
          .text('User created successfully!');

        $form.after($message);
        $message.fadeIn().delay(3000).fadeOut();

        // Clear form
        $inputs.val('');
      },
      error: function(xhr) {
        $('.error-message').text(xhr.responseText).show();
      }
    });
  });

  // Show/hide password
  $('.toggle-password').on('click', function() {
    var $input = $(this).siblings('input');
    var type = $input.attr('type');
    $input.attr('type', type === 'password' ? 'text' : 'password');
    $(this).toggleClass('active');
  });
});

After (Vanilla JavaScript):

// app.ts - Modern vanilla JavaScript
document.addEventListener('DOMContentLoaded', () => {
  // DOM selection - Native APIs
  const button = document.querySelector<HTMLButtonElement>('#submit-button');
  const form = document.querySelector<HTMLFormElement>('.user-form');
  const inputs = form?.querySelectorAll<HTMLInputElement>('input');

  if (!button || !form || !inputs) return;

  // Event handling - addEventListener
  button.addEventListener('click', async (e) => {
    e.preventDefault();

    // Get form data - FormData API
    const formData = new FormData(form);
    const data = Object.fromEntries(formData.entries());

    try {
      // Fetch API instead of $.ajax
      const response = await fetch('/api/users', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(data)
      });

      if (!response.ok) {
        throw new Error(await response.text());
      }

      const user = await response.json();

      // DOM manipulation - Native APIs
      const message = document.createElement('div');
      message.className = 'success-message';
      message.textContent = 'User created successfully!';

      form.insertAdjacentElement('afterend', message);

      // CSS animations instead of jQuery animations
      message.style.opacity = '0';
      message.style.display = 'block';

      requestAnimationFrame(() => {
        message.style.transition = 'opacity 0.3s';
        message.style.opacity = '1';

        setTimeout(() => {
          message.style.opacity = '0';
          setTimeout(() => message.remove(), 300);
        }, 3000);
      });

      // Clear form
      form.reset();

    } catch (error) {
      const errorMessage = document.querySelector('.error-message');
      if (errorMessage) {
        errorMessage.textContent = error.message;
        errorMessage.style.display = 'block';
      }
    }
  });

  // Show/hide password - Native APIs
  const toggleButtons = document.querySelectorAll<HTMLButtonElement>('.toggle-password');

  toggleButtons.forEach(toggle => {
    toggle.addEventListener('click', () => {
      const input = toggle.previousElementSibling as HTMLInputElement;
      if (!input) return;

      const type = input.type === 'password' ? 'text' : 'password';
      input.type = type;
      toggle.classList.toggle('active');
    });
  });
});

Bundle size impact:

  • Before: ~30KB (jQuery minified + gzipped)
  • After: ~0KB (native APIs)
  • Savings: 30KB, faster load time

Improvements:

  • No jQuery dependency
  • Modern native APIs
  • Better performance
  • TypeScript support
  • Smaller bundle size

Target 6: React Class Components to Hooks

When to modernize:

  • Using class components
  • Want simpler code
  • Better functional composition
  • Modern React patterns

Before (Class component):

// UserProfile.tsx
import React, { Component } from 'react';

interface Props {
  userId: string;
}

interface State {
  user: User | null;
  loading: boolean;
  error: Error | null;
}

class UserProfile extends Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = {
      user: null,
      loading: true,
      error: null
    };

    this.handleRefresh = this.handleRefresh.bind(this);
  }

  componentDidMount() {
    this.loadUser();
  }

  componentDidUpdate(prevProps: Props) {
    if (prevProps.userId !== this.props.userId) {
      this.loadUser();
    }
  }

  async loadUser() {
    this.setState({ loading: true, error: null });

    try {
      const response = await fetch(`/api/users/${this.props.userId}`);
      const user = await response.json();
      this.setState({ user, loading: false });
    } catch (error) {
      this.setState({ error, loading: false });
    }
  }

  handleRefresh() {
    this.loadUser();
  }

  render() {
    const { user, loading, error } = this.state;

    if (loading) return <div>Loading...</div>;
    if (error) return <div>Error: {error.message}</div>;
    if (!user) return <div>User not found</div>;

    return (
      <div className="user-profile">
        <h1>{user.name}</h1>
        <p>{user.email}</p>
        <button onClick={this.handleRefresh}>Refresh</button>
      </div>
    );
  }
}

export default UserProfile;

After (Function component with hooks):

// UserProfile.tsx
import { useState, useEffect } from 'react';

interface Props {
  userId: string;
}

export function UserProfile({ userId }: Props) {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  // Extract to custom hook for reusability
  const loadUser = async () => {
    setLoading(true);
    setError(null);

    try {
      const response = await fetch(`/api/users/${userId}`);
      const userData = await response.json();
      setUser(userData);
    } catch (err) {
      setError(err as Error);
    } finally {
      setLoading(false);
    }
  };

  // Load user on mount and when userId changes
  useEffect(() => {
    loadUser();
  }, [userId]);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  if (!user) return <div>User not found</div>;

  return (
    <div className="user-profile">
      <h1>{user.name}</h1>
      <p>{user.email}</p>
      <button onClick={loadUser}>Refresh</button>
    </div>
  );
}

Even better with custom hook:

// hooks/useUser.ts
import { useState, useEffect } from 'react';

export function useUser(userId: string) {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  const loadUser = async () => {
    setLoading(true);
    setError(null);

    try {
      const response = await fetch(`/api/users/${userId}`);
      const userData = await response.json();
      setUser(userData);
    } catch (err) {
      setError(err as Error);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    loadUser();
  }, [userId]);

  return { user, loading, error, refresh: loadUser };
}

// UserProfile.tsx - Super clean now!
export function UserProfile({ userId }: { userId: string }) {
  const { user, loading, error, refresh } = useUser(userId);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  if (!user) return <div>User not found</div>;

  return (
    <div className="user-profile">
      <h1>{user.name}</h1>
      <p>{user.email}</p>
      <button onClick={refresh}>Refresh</button>
    </div>
  );
}

Improvements:

  • Less boilerplate: No constructor, bind, etc.
  • Simpler: Function instead of class
  • Reusability: Custom hooks
  • Better composition: Hooks compose well
  • Modern: Current React best practice

Output Format

# Legacy Code Modernization Report

## Targets Modernized: <target1, target2, ...>

**Scope**: <path>
**Compatibility**: <version>

## Before Modernization

**Legacy Patterns Found**:
- var declarations: <count>
- Callback functions: <count>
- Prototype usage: <count>
- CommonJS modules: <count>
- jQuery usage: <count>
- Class components: <count>

**Issues**:
- Callback hell in <count> files
- Poor error handling
- Large bundle size (jQuery dependency)
- Outdated syntax

## Modernization Performed

### Target 1: <target-name>

**Files Modified**: <count>

**Before**:
```javascript
<legacy-code>

After:

<modern-code>

Improvements:

  • <improvement 1>
  • <improvement 2>

Target 2:

[Same structure...]

After Modernization

Modern Patterns:

  • const/let: conversions
  • Async/await: conversions
  • ES6 classes: conversions
  • ES modules: conversions
  • Vanilla JS: jQuery removals
  • Function components: conversions

Metrics:

  • Bundle size: KB → KB (% reduction)
  • Code quality: Significantly improved
  • Maintainability: Much easier
  • Performance:

Testing

Tests Updated: All tests passing: YES New tests added:

Breaking Changes

Migration Guide

For Consumers:

// Old API
<old-usage>

// New API
<new-usage>

Next Steps

Further Modernization:


Modernization Complete: Codebase updated to modern standards.


## Error Handling

**Incompatible environment**:

Error: Target environment does not support

Target: Required:

Options:

  1. Use Babel/TypeScript to transpile
  2. Update target environment
  3. Choose different modernization target

**Too many changes**:

Warning: Modernizing files is a large change.

Recommendation: Gradual modernization

  1. Start with critical paths
  2. Modernize incrementally
  3. Test thoroughly between changes
  4. Review with team