javascript

exercises

exercises.js
/**
 * ============================================================
 * 22.4 ES MODULES INTERNALS - EXERCISES
 * ============================================================
 *
 * Practice understanding ES Module loading, linking,
 * and evaluation phases.
 */

/**
 * EXERCISE 1: MODULE DEPENDENCY GRAPH
 *
 * Build a dependency graph from module imports.
 * Given module definitions, create a graph showing dependencies.
 *
 * Requirements:
 * - Parse import statements (simplified format)
 * - Build adjacency list representation
 * - Detect circular dependencies
 */

console.log('=== Exercise 1: Module Dependency Graph ===\n');

class ModuleDependencyGraph {
  constructor() {
    this.modules = new Map(); // name -> { imports: [], exports: [] }
  }

  /**
   * Add a module with its import statements
   * @param {string} name - Module name
   * @param {string[]} imports - Array of module names this imports
   */
  addModule(name, imports = []) {
    // TODO: Add module to graph
    // Store its imports as dependencies
  }

  /**
   * Get all dependencies (direct and transitive) for a module
   * @param {string} name - Module name
   * @returns {string[]} - All dependencies
   */
  getAllDependencies(name) {
    // TODO: Return all dependencies (direct and transitive)
    // Use BFS or DFS to traverse the graph
    return [];
  }

  /**
   * Detect if there's a circular dependency involving the given module
   * @param {string} name - Module to check
   * @returns {boolean} - True if circular dependency exists
   */
  hasCircularDependency(name) {
    // TODO: Detect circular dependencies
    // Use DFS with visited/inStack tracking
    return false;
  }

  /**
   * Get topological order for loading modules
   * @param {string} entry - Entry module name
   * @returns {string[]} - Modules in load order
   */
  getLoadOrder(entry) {
    // TODO: Return modules in topological order
    // Dependencies should come before dependents
    return [];
  }
}

// Test setup
const graph = new ModuleDependencyGraph();
graph.addModule('main', ['router', 'store']);
graph.addModule('router', ['utils', 'components']);
graph.addModule('store', ['utils']);
graph.addModule('components', ['utils', 'styles']);
graph.addModule('utils', []);
graph.addModule('styles', []);

console.log('Graph setup:');
console.log('  main → router, store');
console.log('  router → utils, components');
console.log('  store → utils');
console.log('  components → utils, styles');
console.log('  utils → (none)');
console.log('  styles → (none)\n');

console.log("All dependencies of 'main':", graph.getAllDependencies('main'));
console.log('Has circular?:', graph.hasCircularDependency('main'));
console.log('Load order:', graph.getLoadOrder('main'));
console.log();

/**
 * EXERCISE 2: LIVE BINDING SIMULATOR
 *
 * Create a system that simulates ES Module live bindings.
 * Exports should be read-through, not copied values.
 */

console.log('=== Exercise 2: Live Binding Simulator ===\n');

class LiveBindingModule {
  constructor() {
    this._exports = new Map();
    this._values = new Map();
  }

  /**
   * Define an export with a getter
   * @param {string} name - Export name
   * @param {Function} getter - Function returning current value
   */
  defineExport(name, getter) {
    // TODO: Store the getter for live access
  }

  /**
   * Get an export's current value
   * @param {string} name - Export name
   * @returns {*} - Current value
   */
  getExport(name) {
    // TODO: Return live value by calling getter
    return undefined;
  }

  /**
   * Create a namespace object for import *
   * @returns {Object} - Frozen namespace object
   */
  createNamespace() {
    // TODO: Create a frozen object with getters for all exports
    return {};
  }
}

// Test
const counterModule = new LiveBindingModule();
let internalCount = 0;
counterModule.defineExport('count', () => internalCount);
counterModule.defineExport('increment', () => () => {
  internalCount++;
});

console.log('Initial count:', counterModule.getExport('count'));
counterModule.getExport('increment')();
console.log('After increment:', counterModule.getExport('count'));
counterModule.getExport('increment')();
counterModule.getExport('increment')();
console.log('After 2 more:', counterModule.getExport('count'));

const ns = counterModule.createNamespace();
console.log('Namespace is frozen:', Object.isFrozen(ns));
console.log();

/**
 * EXERCISE 3: MODULE LOADING PHASES
 *
 * Implement the three phases of module loading:
 * 1. Construction (parse and create module record)
 * 2. Instantiation (create environment, link imports/exports)
 * 3. Evaluation (execute code)
 */

console.log('=== Exercise 3: Module Loading Phases ===\n');

class ModuleLoader {
  constructor() {
    this.moduleMap = new Map(); // url -> module record
    this.loadingStack = [];
  }

  /**
   * Phase 1: Construction
   * Parse module and create module record
   */
  construct(url, source) {
    console.log(`  [Construct] ${url}`);

    // TODO: Parse source to extract:
    // - imports (module specifiers and bindings)
    // - exports (names and local bindings)
    // - code body

    const moduleRecord = {
      url,
      status: 'uninstantiated',
      imports: [], // Parse these from source
      exports: [], // Parse these from source
      body: source, // Actual code to evaluate
      environment: null,
    };

    this.moduleMap.set(url, moduleRecord);
    return moduleRecord;
  }

  /**
   * Phase 2: Instantiation
   * Create module environment and link bindings
   */
  instantiate(url) {
    const record = this.moduleMap.get(url);
    if (!record) throw new Error(`Module not found: ${url}`);

    console.log(`  [Instantiate] ${url}`);

    // TODO:
    // 1. Create module environment record
    // 2. Recursively instantiate dependencies
    // 3. Link import bindings to export bindings

    record.status = 'linked';
    record.environment = {}; // Create actual environment

    return record;
  }

  /**
   * Phase 3: Evaluation
   * Execute module code
   */
  evaluate(url) {
    const record = this.moduleMap.get(url);
    if (!record) throw new Error(`Module not found: ${url}`);
    if (record.status === 'evaluated') return record;

    console.log(`  [Evaluate] ${url}`);

    // TODO:
    // 1. Recursively evaluate dependencies first
    // 2. Execute module body
    // 3. Mark as evaluated

    record.status = 'evaluated';
    return record;
  }

  /**
   * Load a module through all phases
   */
  load(url, source) {
    console.log(`Loading: ${url}\n`);
    this.construct(url, source);
    this.instantiate(url);
    this.evaluate(url);
    console.log(`Module ${url} loaded!\n`);
    return this.moduleMap.get(url);
  }
}

const loader = new ModuleLoader();

const mathSource = `
  export const PI = 3.14159;
  export function add(a, b) { return a + b; }
`;

loader.load('./math.js', mathSource);
console.log();

/**
 * EXERCISE 4: CIRCULAR DEPENDENCY HANDLER
 *
 * Handle circular dependencies like ES Modules do.
 * Partially evaluated exports should be accessible.
 */

console.log('=== Exercise 4: Circular Dependency Handler ===\n');

class CircularAwareLoader {
  constructor() {
    this.modules = new Map();
    this.evaluating = new Set();
  }

  register(name, dependencies, init) {
    this.modules.set(name, {
      dependencies,
      init,
      exports: null,
      evaluated: false,
    });
  }

  /**
   * Load module handling circular dependencies
   * @param {string} name - Module name
   * @returns {Object} - Module exports
   */
  require(name) {
    const module = this.modules.get(name);
    if (!module) throw new Error(`Unknown module: ${name}`);

    // TODO: Handle these cases:
    // 1. Already evaluated - return exports
    // 2. Currently evaluating (circular) - return partial exports
    // 3. Not started - evaluate with dependency resolution

    // Hint: Track evaluating modules in this.evaluating
    // Create exports object BEFORE evaluating body
    // This allows circular refs to access partial exports

    return module.exports || {};
  }
}

// Setup circular modules
const circLoader = new CircularAwareLoader();

circLoader.register('a', ['b'], (exports, b) => {
  exports.name = 'Module A';
  exports.bName = b.name; // Should work if handled correctly
});

circLoader.register('b', ['a'], (exports, a) => {
  exports.name = 'Module B';
  exports.aName = a.name; // undefined during first evaluation
  exports.getAName = () => a.name; // Works because of live binding
});

console.log("Loading 'a' (circular with 'b'):");
const modA = circLoader.require('a');
const modB = circLoader.require('b');

console.log('a.name:', modA.name);
console.log('b.name:', modB.name);
console.log('b.aName:', modB.aName); // Might be undefined
console.log('b.getAName():', modB.getAName()); // Should work
console.log();

/**
 * EXERCISE 5: IMPORT MAP RESOLVER
 *
 * Implement import map resolution for bare specifiers.
 * Import maps allow controlling module resolution.
 */

console.log('=== Exercise 5: Import Map Resolver ===\n');

class ImportMapResolver {
  constructor(importMap = {}) {
    this.imports = importMap.imports || {};
    this.scopes = importMap.scopes || {};
  }

  /**
   * Resolve a specifier using import map
   * @param {string} specifier - The import specifier
   * @param {string} referrer - The importing module URL
   * @returns {string} - Resolved URL
   */
  resolve(specifier, referrer) {
    // TODO: Implement import map resolution:
    // 1. Check scoped imports first (based on referrer)
    // 2. Check top-level imports
    // 3. Handle prefix matching (e.g., 'lodash/' → 'https://...')
    // 4. Fall back to specifier if no match

    return specifier;
  }
}

const importMap = {
  imports: {
    lodash: 'https://cdn.example.com/lodash@4.17.21/lodash.min.js',
    'lodash/': 'https://cdn.example.com/lodash@4.17.21/',
    react: 'https://esm.sh/react@18.2.0',
    '@myorg/': 'https://myorg.com/packages/',
  },
  scopes: {
    '/admin/': {
      react: 'https://esm.sh/react@17.0.2', // Admin uses older React
    },
  },
};

const resolver = new ImportMapResolver(importMap);

console.log('Import map resolution:');
console.log("  'lodash' →", resolver.resolve('lodash', '/src/app.js'));
console.log(
  "  'lodash/debounce' →",
  resolver.resolve('lodash/debounce', '/src/app.js')
);
console.log(
  "  'react' (from /src/) →",
  resolver.resolve('react', '/src/app.js')
);
console.log(
  "  'react' (from /admin/) →",
  resolver.resolve('react', '/admin/dashboard.js')
);
console.log(
  "  '@myorg/utils' →",
  resolver.resolve('@myorg/utils', '/src/app.js')
);
console.log();

/**
 * EXERCISE 6: STATIC ANALYSIS FOR TREE-SHAKING
 *
 * Analyze module exports and determine which can be removed.
 * Simulate basic tree-shaking analysis.
 */

console.log('=== Exercise 6: Tree-Shaking Analysis ===\n');

class TreeShaker {
  constructor() {
    this.modules = new Map();
  }

  /**
   * Add a module with its exports
   */
  addModule(name, exports) {
    this.modules.set(name, {
      exports,
      usedExports: new Set(),
    });
  }

  /**
   * Mark exports as used based on import statements
   */
  markUsed(imports) {
    // imports: [{ from: 'module', names: ['a', 'b'] }, ...]
    // TODO: For each import, mark those exports as used
  }

  /**
   * Get unused exports that can be tree-shaken
   */
  getUnusedExports() {
    // TODO: Return map of module -> unused exports
    return {};
  }

  /**
   * Calculate size savings
   */
  calculateSavings() {
    // TODO: Calculate percentage of code that can be removed
    return { removed: 0, total: 0, percentage: 0 };
  }
}

const shaker = new TreeShaker();

// Add modules with their exports and sizes
shaker.addModule('utils', {
  formatDate: { size: 500 },
  formatNumber: { size: 300 },
  formatCurrency: { size: 400 },
  debounce: { size: 200 },
  throttle: { size: 200 },
  deepClone: { size: 600 },
});

shaker.addModule('math', {
  add: { size: 50 },
  subtract: { size: 50 },
  multiply: { size: 50 },
  divide: { size: 50 },
  pow: { size: 100 },
  sqrt: { size: 100 },
  PI: { size: 10 },
  E: { size: 10 },
});

// Mark what's actually used
shaker.markUsed([
  { from: 'utils', names: ['formatDate', 'debounce'] },
  { from: 'math', names: ['add', 'PI'] },
]);

console.log('Unused exports:', shaker.getUnusedExports());
console.log('Savings:', shaker.calculateSavings());
console.log();

/**
 * EXERCISE 7: DYNAMIC IMPORT ORCHESTRATOR
 *
 * Build a system to manage dynamic imports with:
 * - Preloading
 * - Retries
 * - Caching
 * - Progress tracking
 */

console.log('=== Exercise 7: Dynamic Import Orchestrator ===\n');

class DynamicImportOrchestrator {
  constructor() {
    this.cache = new Map();
    this.pending = new Map();
    this.preloadQueue = [];
  }

  /**
   * Dynamically import a module with options
   */
  async import(specifier, options = {}) {
    const {
      timeout = 5000,
      retries = 3,
      onProgress = () => {},
      priority = 'normal',
    } = options;

    // TODO: Implement with:
    // 1. Check cache first
    // 2. Avoid duplicate requests (check pending)
    // 3. Handle retries on failure
    // 4. Timeout handling
    // 5. Progress reporting

    return {};
  }

  /**
   * Preload modules for later use
   */
  preload(specifiers) {
    // TODO: Queue specifiers for background loading
  }

  /**
   * Clear cache for module
   */
  invalidate(specifier) {
    // TODO: Remove from cache to force reload
  }

  /**
   * Get cache stats
   */
  getStats() {
    // TODO: Return cache hit/miss stats
    return { hits: 0, misses: 0, cached: 0 };
  }
}

const orchestrator = new DynamicImportOrchestrator();

// Simulate usage
console.log('Stats:', orchestrator.getStats());
console.log();

/**
 * EXERCISE 8: MODULE FEDERATION SIMULATOR
 *
 * Simulate the module federation pattern where modules
 * can be loaded from different applications at runtime.
 */

console.log('=== Exercise 8: Module Federation ===\n');

class ModuleFederation {
  constructor(name) {
    this.name = name;
    this.exposes = new Map(); // Local modules exposed to others
    this.remotes = new Map(); // Remote containers
    this.shared = new Map(); // Shared modules
  }

  /**
   * Expose a local module
   */
  expose(path, module) {
    // TODO: Register module to be consumed by remotes
  }

  /**
   * Register a remote container
   */
  registerRemote(name, containerPromise) {
    // TODO: Store reference to remote container
  }

  /**
   * Get a module from a remote
   */
  async getRemoteModule(remote, path) {
    // TODO: Load module from remote container
    return {};
  }

  /**
   * Share a module (version negotiation)
   */
  share(name, module, version) {
    // TODO: Register shared module with version
    // Implement version negotiation logic
  }

  /**
   * Get shared module (prefer higher version)
   */
  getShared(name, requiredVersion) {
    // TODO: Return compatible shared module
    return null;
  }
}

const hostApp = new ModuleFederation('host');
const remoteApp = new ModuleFederation('remote');

// Remote exposes components
remoteApp.expose('./Button', { Button: 'RemoteButton' });
remoteApp.expose('./Card', { Card: 'RemoteCard' });

// Host registers remote
hostApp.registerRemote('remote', Promise.resolve(remoteApp));

console.log('Module Federation setup complete');
console.log();

/**
 * EXERCISE 9: IMPLEMENT import.meta.hot
 *
 * Create a Hot Module Replacement (HMR) API
 * similar to Vite's or webpack's.
 */

console.log('=== Exercise 9: HMR API (import.meta.hot) ===\n');

class HotModuleAPI {
  constructor(moduleId) {
    this.moduleId = moduleId;
    this.acceptCallbacks = [];
    this.disposeCallbacks = [];
    this.data = {};
  }

  /**
   * Accept updates for this module
   */
  accept(callback) {
    // TODO: Register callback for when module updates
  }

  /**
   * Accept updates for specific dependencies
   */
  acceptDeps(deps, callback) {
    // TODO: Register callback for dependency updates
  }

  /**
   * Cleanup before module is replaced
   */
  dispose(callback) {
    // TODO: Register cleanup callback
  }

  /**
   * Invalidate forces parent to be updated too
   */
  invalidate() {
    // TODO: Mark this module and parents for refresh
  }

  /**
   * Decline HMR (forces full reload)
   */
  decline() {
    // TODO: Mark module as non-HMR-capable
  }

  /**
   * Simulate a hot update
   */
  _triggerUpdate(newModule) {
    // TODO: Execute dispose callbacks
    // TODO: Execute accept callbacks with new module
  }
}

const hot = new HotModuleAPI('./component.js');

// Usage example
hot.accept((newModule) => {
  console.log('Module updated:', newModule);
});

hot.dispose((data) => {
  data.savedState = { count: 42 };
  console.log('Cleaning up, saving state');
});

console.log('HMR API ready for', hot.moduleId);
console.log();

/**
 * EXERCISE 10: COMPLETE MODULE BUNDLER
 *
 * Build a simplified module bundler that:
 * - Resolves dependencies
 * - Bundles modules
 * - Handles circular deps
 * - Outputs single file
 */

console.log('=== Exercise 10: Module Bundler ===\n');

class SimpleBundler {
  constructor() {
    this.modules = new Map();
    this.moduleId = 0;
  }

  /**
   * Parse a module and extract dependencies
   */
  parse(code) {
    // TODO: Extract import statements
    // Return { imports: [...], code: '...' }
    return { imports: [], code };
  }

  /**
   * Add a module to the bundle
   */
  addModule(path, code) {
    const id = this.moduleId++;
    const { imports, code: transformedCode } = this.parse(code);

    this.modules.set(path, {
      id,
      path,
      imports,
      code: transformedCode,
    });

    return id;
  }

  /**
   * Build the dependency graph
   */
  buildGraph(entry) {
    // TODO: Start from entry, recursively add all dependencies
    return [];
  }

  /**
   * Generate the bundle output
   */
  bundle(entry) {
    const graph = this.buildGraph(entry);

    // TODO: Generate runtime that:
    // 1. Defines a module registry
    // 2. Wraps each module in a function
    // 3. Handles require() calls
    // 4. Starts from entry module

    const runtime = `
      (function(modules) {
        // TODO: Implement module runtime
        const cache = {};
        
        function require(id) {
          // Check cache
          // Execute module
          // Return exports
        }
        
        // Start from entry
        require(0);
      })({
        // Module definitions here
      });
    `;

    return runtime;
  }
}

const bundler = new SimpleBundler();

bundler.addModule(
  './index.js',
  `
  import { add } from './math.js';
  console.log(add(1, 2));
`
);

bundler.addModule(
  './math.js',
  `
  export function add(a, b) { return a + b; }
`
);

console.log('Bundle output (simplified):');
console.log(bundler.bundle('./index.js').slice(0, 200) + '...');
console.log();

/**
 * ============================================================
 * SOLUTIONS
 * ============================================================
 */

console.log('=== SOLUTIONS ===\n');

// Solution 1: Module Dependency Graph
console.log('--- Solution 1: Module Dependency Graph ---\n');

class ModuleDependencyGraphSolution {
  constructor() {
    this.modules = new Map();
  }

  addModule(name, imports = []) {
    this.modules.set(name, { imports });
  }

  getAllDependencies(name, visited = new Set()) {
    if (visited.has(name)) return [];
    visited.add(name);

    const module = this.modules.get(name);
    if (!module) return [];

    let deps = [...module.imports];
    for (const imp of module.imports) {
      deps = deps.concat(this.getAllDependencies(imp, visited));
    }

    return [...new Set(deps)];
  }

  hasCircularDependency(name, visited = new Set(), stack = new Set()) {
    if (stack.has(name)) return true;
    if (visited.has(name)) return false;

    visited.add(name);
    stack.add(name);

    const module = this.modules.get(name);
    if (module) {
      for (const dep of module.imports) {
        if (this.hasCircularDependency(dep, visited, stack)) {
          return true;
        }
      }
    }

    stack.delete(name);
    return false;
  }

  getLoadOrder(entry) {
    const order = [];
    const visited = new Set();

    const visit = (name) => {
      if (visited.has(name)) return;
      visited.add(name);

      const module = this.modules.get(name);
      if (module) {
        for (const dep of module.imports) {
          visit(dep);
        }
      }
      order.push(name);
    };

    visit(entry);
    return order;
  }
}

const graphSol = new ModuleDependencyGraphSolution();
graphSol.addModule('main', ['router', 'store']);
graphSol.addModule('router', ['utils', 'components']);
graphSol.addModule('store', ['utils']);
graphSol.addModule('components', ['utils', 'styles']);
graphSol.addModule('utils', []);
graphSol.addModule('styles', []);

console.log("All dependencies of 'main':", graphSol.getAllDependencies('main'));
console.log('Has circular?:', graphSol.hasCircularDependency('main'));
console.log('Load order:', graphSol.getLoadOrder('main'));

// Add circular dependency
graphSol.addModule('a', ['b']);
graphSol.addModule('b', ['a']);
console.log('Has circular (a)?:', graphSol.hasCircularDependency('a'));
console.log();

// Solution 2: Live Binding Simulator
console.log('--- Solution 2: Live Binding Simulator ---\n');

class LiveBindingModuleSolution {
  constructor() {
    this._getters = new Map();
  }

  defineExport(name, getter) {
    this._getters.set(name, getter);
  }

  getExport(name) {
    const getter = this._getters.get(name);
    return getter ? getter() : undefined;
  }

  createNamespace() {
    const ns = Object.create(null);

    for (const [name, getter] of this._getters) {
      Object.defineProperty(ns, name, {
        get: getter,
        enumerable: true,
        configurable: false,
      });
    }

    Object.defineProperty(ns, Symbol.toStringTag, {
      value: 'Module',
      configurable: false,
    });

    return Object.freeze(ns);
  }
}

const counterModSol = new LiveBindingModuleSolution();
let countSol = 0;
counterModSol.defineExport('count', () => countSol);
counterModSol.defineExport('increment', () => () => {
  countSol++;
});

console.log('Initial:', counterModSol.getExport('count'));
counterModSol.getExport('increment')();
console.log('After increment:', counterModSol.getExport('count'));

const nsSol = counterModSol.createNamespace();
counterModSol.getExport('increment')();
console.log('Via namespace:', nsSol.count); // Live binding!
console.log('Is frozen:', Object.isFrozen(nsSol));
console.log();

// Solution 5: Import Map Resolver
console.log('--- Solution 5: Import Map Resolver ---\n');

class ImportMapResolverSolution {
  constructor(importMap = {}) {
    this.imports = importMap.imports || {};
    this.scopes = importMap.scopes || {};
  }

  resolve(specifier, referrer) {
    // Check scopes first
    for (const [scopePrefix, scopeImports] of Object.entries(this.scopes)) {
      if (referrer.startsWith(scopePrefix)) {
        const result = this._matchImport(specifier, scopeImports);
        if (result) return result;
      }
    }

    // Check top-level imports
    const result = this._matchImport(specifier, this.imports);
    if (result) return result;

    return specifier;
  }

  _matchImport(specifier, imports) {
    // Exact match
    if (imports[specifier]) {
      return imports[specifier];
    }

    // Prefix match (ends with /)
    for (const [key, value] of Object.entries(imports)) {
      if (key.endsWith('/') && specifier.startsWith(key)) {
        return value + specifier.slice(key.length);
      }
    }

    return null;
  }
}

const resolverSol = new ImportMapResolverSolution(importMap);

console.log("'lodash' →", resolverSol.resolve('lodash', '/src/app.js'));
console.log(
  "'lodash/debounce' →",
  resolverSol.resolve('lodash/debounce', '/src/app.js')
);
console.log(
  "'react' (from /src/) →",
  resolverSol.resolve('react', '/src/app.js')
);
console.log(
  "'react' (from /admin/) →",
  resolverSol.resolve('react', '/admin/dashboard.js')
);
console.log(
  "'@myorg/utils' →",
  resolverSol.resolve('@myorg/utils', '/src/app.js')
);
console.log();

// Solution 6: Tree-Shaking Analysis
console.log('--- Solution 6: Tree-Shaking Analysis ---\n');

class TreeShakerSolution {
  constructor() {
    this.modules = new Map();
  }

  addModule(name, exports) {
    this.modules.set(name, {
      exports,
      usedExports: new Set(),
    });
  }

  markUsed(imports) {
    for (const { from, names } of imports) {
      const module = this.modules.get(from);
      if (module) {
        names.forEach((name) => module.usedExports.add(name));
      }
    }
  }

  getUnusedExports() {
    const unused = {};
    for (const [name, module] of this.modules) {
      const unusedNames = Object.keys(module.exports).filter(
        (exp) => !module.usedExports.has(exp)
      );
      if (unusedNames.length > 0) {
        unused[name] = unusedNames;
      }
    }
    return unused;
  }

  calculateSavings() {
    let removed = 0;
    let total = 0;

    for (const [_, module] of this.modules) {
      for (const [exp, { size }] of Object.entries(module.exports)) {
        total += size;
        if (!module.usedExports.has(exp)) {
          removed += size;
        }
      }
    }

    return {
      removed,
      total,
      percentage: total > 0 ? Math.round((removed / total) * 100) : 0,
    };
  }
}

const shakerSol = new TreeShakerSolution();

shakerSol.addModule('utils', {
  formatDate: { size: 500 },
  formatNumber: { size: 300 },
  formatCurrency: { size: 400 },
  debounce: { size: 200 },
  throttle: { size: 200 },
  deepClone: { size: 600 },
});

shakerSol.addModule('math', {
  add: { size: 50 },
  subtract: { size: 50 },
  multiply: { size: 50 },
  divide: { size: 50 },
  pow: { size: 100 },
  sqrt: { size: 100 },
  PI: { size: 10 },
  E: { size: 10 },
});

shakerSol.markUsed([
  { from: 'utils', names: ['formatDate', 'debounce'] },
  { from: 'math', names: ['add', 'PI'] },
]);

console.log('Unused exports:', shakerSol.getUnusedExports());
console.log('Savings:', shakerSol.calculateSavings());
console.log();

console.log('=== Exercises Complete ===\n');
Exercises - JavaScript Tutorial | DeepML