Skip to content

WIP: Module Reloading Stratagies #3809

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 15 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions apps/vm-hotreload/module-based/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Module-Based Hot Reload Demo

This example demonstrates hot reloading using Node.js module system overrides and VM contexts. It intercepts the `require()` mechanism to create live bindings that automatically update when files change.

## How It Works

- **Module Override**: Overrides `Module._extensions['.js']` to intercept module loading
- **VM Context**: Uses Node.js `vm` module to execute code in isolated contexts
- **Live Bindings**: Creates getter-based exports that dynamically fetch from VM instances
- **File Watching**: Uses `chokidar` to watch for file changes and destroy VM instances

## Key Features

- ✅ True hot reloading without restart
- ✅ Fresh state on each reload (VM recreation)
- ✅ Automatic file watching
- ✅ Clean VM isolation
- ✅ Live getter-based exports

## Files

- `index.js` - Main demo runner with file watching and iteration logic
- `register-vm-loader.js` - Core hot reload implementation using VM contexts
- `entrypoint1.js` - Demo module 1 (simple state and functions)
- `entrypoint2.js` - Demo module 2 (simple state and functions)

## Usage

```bash
npm install
npm start
```

The demo will:
1. Load both entrypoint modules
2. Run 3 iterations, automatically modifying the greet messages
3. Show how modules are hot reloaded with fresh VM instances
4. Display live updates as files change

## Technical Details

### VM Context Approach

The `register-vm-loader.js` creates a custom module loader that:

1. **Intercepts Module Loading**: Overrides the default `.js` extension handler
2. **Creates VM Instances**: Runs module code in isolated VM contexts
3. **Provides Live Bindings**: Uses getters to dynamically access VM exports
4. **Manages Lifecycle**: Destroys and recreates VMs when files change

### Hot Reload Flow

1. File change detected by `chokidar`
2. VM instance destroyed via `destroyVM()`
3. Next property access triggers VM recreation with fresh state
4. New code executed in fresh VM context
5. Live bindings automatically reflect new exports

## Advantages

- **True Hot Reloading**: No application restart required
- **State Isolation**: Each module runs in its own VM context
- **Automatic Updates**: Live bindings update automatically
- **Clean Architecture**: Clear separation between loader and application logic

## Use Cases

- Development environments requiring fast iteration
- Applications with expensive startup costs
- Scenarios where fresh state on reload is acceptable
- Testing module isolation and hot reload mechanisms
17 changes: 17 additions & 0 deletions apps/vm-hotreload/module-based/entrypoint1.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// entrypoint1.js (module-based, no VM awareness)
console.log('🚀 Entrypoint 1 loaded (module-based)');

const state = {
name: 'Entrypoint 1',
counter: 0,
createdAt: new Date().toISOString(),
};

module.exports = {
getName: () => state.name,
increment: () => ++state.counter,
getCounter: () => state.counter,
getCreatedAt: () => state.createdAt,
greet: (name = 'World') =>
`🔥 HOT RELOADED: Hey ${name}! Iteration ${state.counter}`,
};
16 changes: 16 additions & 0 deletions apps/vm-hotreload/module-based/entrypoint2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// entrypoint2.js (module-based, no VM awareness)
console.log('🚀 Entrypoint 2 loaded (module-based)');

const state = {
name: 'Entrypoint 2',
counter: 100,
createdAt: new Date().toISOString(),
};

module.exports = {
getName: () => state.name,
increment: () => ++state.counter,
getCounter: () => state.counter,
getCreatedAt: () => state.createdAt,
greet: (name = 'Universe') => `Welcome ${name} to ${state.name}!`,
};
112 changes: 112 additions & 0 deletions apps/vm-hotreload/module-based/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
const chokidar = require('chokidar');
const path = require('path');
const fs = require('fs');

const { registerHotReload, destroyVM } = require('./register-vm-loader');

registerHotReload(['entrypoint1.js', 'entrypoint2.js']);

let entrypoint1 = require('./entrypoint1');
let entrypoint2 = require('./entrypoint2');

const entrypointPaths = [
path.resolve(__dirname, 'entrypoint1.js'),
path.resolve(__dirname, 'entrypoint2.js'),
];

const watcher = chokidar.watch(entrypointPaths, {
persistent: true,
ignoreInitial: true,
});

watcher.on('change', (filePath) => {
console.log(`📁 File changed: ${path.basename(filePath)}`);
destroyVM(filePath);
if (filePath.endsWith('entrypoint1.js'))
entrypoint1 = require('./entrypoint1');
if (filePath.endsWith('entrypoint2.js'))
entrypoint2 = require('./entrypoint2');
if (typeof continueDemo === 'function') continueDemo();
});

let iteration = 0;
const maxIterations = 3;
let continueDemo = null;

function modifyGreetMessageEntrypoint1(newMessage) {
const filePath = path.resolve(__dirname, 'entrypoint1.js');
let fileContent = fs.readFileSync(filePath, 'utf8');
const greetRegex = /greet: \(name = 'World'\) => `[^`]+`/;
fileContent = fileContent.replace(
greetRegex,
`greet: (name = 'World') => \`${newMessage}\``,
);
fs.writeFileSync(filePath, fileContent);
console.log(`\n📝 Modified entrypoint1 greet message to: "${newMessage}"`);
}

function modifyGreetMessageEntrypoint2(newMessage) {
const filePath = path.resolve(__dirname, 'entrypoint2.js');
let fileContent = fs.readFileSync(filePath, 'utf8');
const greetRegex = /greet: \(name = 'Universe'\) => `[^`]+`/;
fileContent = fileContent.replace(
greetRegex,
`greet: (name = 'Universe') => \`${newMessage}\``,
);
fs.writeFileSync(filePath, fileContent);
console.log(`\n📝 Modified entrypoint2 greet message to: "${newMessage}"`);
}

function runDemo() {
iteration++;
console.log(
`\n🎭 Running Demo (Iteration ${iteration}/${maxIterations})...\n`,
);

try {
console.log('=== Entrypoint 1 Demo ===');
console.log('Name:', entrypoint1.getName());
console.log('Counter:', entrypoint1.getCounter());
console.log('Increment:', entrypoint1.increment());
console.log('Greet:', entrypoint1.greet('Developer'));
console.log('Created at:', entrypoint1.getCreatedAt());

console.log('\n=== Entrypoint 2 Demo ===');
console.log('Name:', entrypoint2.getName());
console.log('Counter:', entrypoint2.getCounter());
console.log('Increment:', entrypoint2.increment());
console.log('Greet:', entrypoint2.greet('Universe'));
console.log('Created at:', entrypoint2.getCreatedAt());
} catch (error) {
console.error('❌ Demo error:', error);
}

if (iteration >= maxIterations) {
console.log('\n✅ Completed all iterations. Exiting...');
watcher.close();
process.exit(0);
} else {
const messages1 = [
'Hello ${name} from ${state.name}! Counter: ${state.counter}',
'🔥 HOT RELOADED: Hey ${name}! Iteration ${state.counter}',
'✨ FINAL: Greetings ${name}! Count is ${state.counter}',
];
const messages2 = [
'Greetings ${name} from ${state.name}! Counter: ${state.counter}',
'Welcome ${name} to ${state.name}!',
'🌟 Universe says hi to ${name}! Counter: ${state.counter}',
];
setTimeout(() => {
modifyGreetMessageEntrypoint1(messages1[iteration]);
modifyGreetMessageEntrypoint2(messages2[iteration]);
continueDemo = runDemo;
}, 1500);
}
}

runDemo();

console.log(
'\n💡 The greet message will be automatically modified between iterations!',
);
console.log('💡 Watch how hot reload picks up the changes without restarting!');
178 changes: 178 additions & 0 deletions apps/vm-hotreload/module-based/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading