javascript
exercises
exercises.js⚡javascript
/**
* ============================================================
* 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');