Last active
September 10, 2023 03:59
-
-
Save marxangels/68c1e50fe144c4ec4c911a6534aa7e32 to your computer and use it in GitHub Desktop.
A simple module-level hot-reload for my express web application with less than 200 lines of code.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import fs from 'fs' | |
import vm from 'vm' | |
import path from 'path' | |
/* | |
* cli command | |
* node --experimental-vm-modules express-hot-reload.js | |
*/ | |
const ModuleCache = new Map() | |
const context = vm.createContext(global) | |
const projRoot = path.join(import.meta.url.substr(7), '../..') | |
globalThis.useHotReload = registerWebHook | |
await SrcModule(projRoot + '/api/index.js') | |
function registerWebHook(app) { | |
app.use('/dev-restart-process', (req, res) => { | |
process.kill(1, 'SIGPIPE') | |
res.send('restarting') | |
}) | |
app.use('/dev-reload-module', async(req, res) => { | |
let { moduleFullPath } = req.query | |
if (undefined === ModuleCache.get(moduleFullPath)) { | |
return res.send({ moduleFullPath, message: 'nothing matched' }) | |
} | |
if (ModuleCache.get(moduleFullPath).namespace.default instanceof Function) { | |
await SrcModule(moduleFullPath, true) | |
res.send({ moduleFullPath, message: 'reloaded' }) | |
} else { | |
process.kill(1, 'SIGPIPE') | |
res.send({ moduleFullPath, message: 'RESTART SIGNAL sent to process 1' }) | |
} | |
}) | |
} | |
async function ProxyModule(filepath) { | |
let proxy = ModuleCache.get('proxy:' + filepath) | |
if (proxy) return proxy | |
let module = await SrcModule(filepath) | |
let names = Object.keys(module.namespace) | |
proxy = new vm.SyntheticModule(names, function() { | |
for (let name of names) { | |
if (module.namespace[name] instanceof Function) { | |
this.setExport(name, new Proxy( | |
function() { | |
return ModuleCache.get(filepath).namespace[name] | |
}, | |
{ | |
has(target, prop) { return prop in target() }, | |
get(target, prop) { return target()[prop] }, | |
set(target, prop, value) { target()[prop] = value; return true }, | |
construct(target, args) { return new (target())(...args) }, | |
apply(target, the, args) { return target().apply(the, args) } | |
})) | |
} else { | |
this.setExport(name, module.namespace[name]) | |
} | |
} | |
}, { context, identifier: 'proxy:' + filepath }) | |
await proxy.link(linker) | |
await proxy.evaluate() | |
ModuleCache.set(proxy.identifier, proxy) | |
return proxy | |
} | |
async function SrcModule(filepath, reload) { | |
let module = ModuleCache.get(filepath) | |
if (module && !reload) return module | |
let code = (await fs.promises.readFile(filepath)).toString('utf8') | |
module = new vm.SourceTextModule(code, { | |
context, | |
identifier: filepath, | |
initializeImportMeta(meta, module) { | |
meta.url = 'file://' + module.identifier | |
}, | |
importModuleDynamically(spec, ref) { | |
return linker(spec, ref) | |
} | |
} | |
) | |
await module.link(linker) | |
await module.evaluate() | |
ModuleCache.set(filepath, module) | |
return module | |
} | |
async function LibModule(spec) { | |
let lib = ModuleCache.get(spec) | |
if (lib) return lib | |
let module = await import(spec) | |
let names = Object.keys(module) | |
lib = new vm.SyntheticModule(names, function() { | |
for (let name of names) this.setExport(name, module[name]) | |
}, { context, identifier: spec }) | |
await lib.link(linker) | |
await lib.evaluate() | |
ModuleCache.set(spec, lib) | |
return lib | |
} | |
function fileStat(file, suf = '') { | |
return fs.promises.lstat(file + suf).catch(() => null) | |
} | |
async function linker(spec, ref) { | |
// handle your path alias | |
if (spec.startsWith('@/')) { | |
spec = spec.replace(/^@\//, projRoot + '/') | |
} else if (spec.startsWith('.')) { | |
spec = path.join(ref.identifier, '..', spec) | |
} else if (spec.startsWith('/')) { | |
// !spec.includes('/node_modules/') | |
} else { | |
return LibModule(spec) | |
} | |
let filepath = path.resolve(spec) | |
if (/\/[-\w]+$/.test(filepath)) { | |
let stat = await fileStat(filepath) | |
if (stat && stat.isDirectory()) { | |
filepath += '/index' | |
} | |
if (await fileStat(filepath, '.js')) { | |
filepath += '.js' | |
} else if (await fileStat(filepath, '.mjs')) { | |
filepath += '.mjs' | |
} | |
} | |
if (/.\.m?js$/.test(filepath)) { | |
let module = await SrcModule(filepath) | |
if (module.namespace.default instanceof Function) { | |
return ProxyModule(filepath) | |
} else { | |
return module | |
} | |
} else { | |
// extend loader | |
return LibModule(spec) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I think this has an issue with the rebuilding of modules,
ie if you have modules A and B; and A imports B,
lets import A, then import B, (a is already build so it uses that cached build)
Now rebuild A, but note B is not gonna get rebuilt, so then B is using a stale version of A.
I'm wondering if I've solved that with something like:
(resolutions is ModuleCache in the above code, and references is a Map that relates a specifier to a Set of specifiers whose modules import that specifier)