UNPKG

hot-hook

Version:

Easy hot module reloading (HMR) for Node.js and ESM

109 lines (108 loc) 3.91 kB
import { register } from 'node:module'; import { MessageChannel } from 'node:worker_threads'; import debug from './debug.js'; class Hot { #options; #messageChannel; #declinePaths = new Set(); #disposeCallbacks = new Map(); #hasOneDeclinedPath(paths) { return paths.some((path) => this.#declinePaths.has(path)); } /** * Handle messages received from the hook's worker thread */ #onMessage(message) { if (message.type === 'hot-hook:full-reload') { process.send?.({ type: 'hot-hook:full-reload', path: message.path, shouldBeReloadable: message.shouldBeReloadable, }); this.#options.onFullReloadAsked?.(); } if (message.type === 'hot-hook:invalidated') { if (this.#hasOneDeclinedPath(message.paths)) { process.send?.({ type: 'hot-hook:full-reload', paths: message.paths }); this.#options.onFullReloadAsked?.(); return; } else { process.send?.({ type: 'hot-hook:invalidated', paths: message.paths }); } for (const url of message.paths) { const callback = this.#disposeCallbacks.get(url); callback?.(); } } } /** * Register the hot reload hooks */ async init(options) { this.#options = Object.assign({ ignore: [ '**/node_modules/**', /** * Vite has a bug where it create multiple files with a * timestamp. This cause hot-hook to restart in loop. * See https://github.com/vitejs/vite/issues/13267 */ '**/vite.config.js.timestamp*', '**/vite.config.ts.timestamp*', ], restart: ['.env'], }, options); debug('Hot hook options %o', this.#options); /** * First, we setup a message channel to be able to communicate * between the hook and the application process since hooks * are running in a worker thread */ this.#messageChannel = new MessageChannel(); register('hot-hook/loader', { parentURL: import.meta.url, transferList: [this.#messageChannel.port2], data: { root: this.#options.root, ignore: this.#options.ignore, restart: this.#options.restart, boundaries: this.#options.boundaries, messagePort: this.#messageChannel.port2, rootDirectory: this.#options.rootDirectory, throwWhenBoundariesAreNotDynamicallyImported: this.#options.throwWhenBoundariesAreNotDynamicallyImported, }, }); this.#messageChannel.port1.on('message', this.#onMessage.bind(this)); } /** * import.meta.hot.dispose internally calls this method * * Dispose is useful for cleaning up resources when a module is reloaded */ dispose(url, callback) { this.#disposeCallbacks.set(new URL(url).pathname, callback); } /** * import.meta.hot.decline internally calls this method * * Decline allows you to mark a module as not reloadable and * will trigger a full server reload when it changes */ decline(url) { this.#declinePaths.add(new URL(url).pathname); } /** * Dump the current state hot hook */ async dump() { this.#messageChannel.port1.postMessage({ type: 'hot-hook:dump' }); const result = await new Promise((resolve) => this.#messageChannel.port1.once('message', (message) => resolve(message))); return result.dump; } } // @ts-ignore const hot = globalThis.hot || new Hot(); // @ts-ignore globalThis.hot = hot; export { hot };