{"id":17743169,"url":"https://github.com/julien-r44/hot-hook","last_synced_at":"2025-06-12T06:05:50.271Z","repository":{"id":230633138,"uuid":"778044365","full_name":"Julien-R44/hot-hook","owner":"Julien-R44","description":"🪝 Simple HMR for NodeJS + ESM","archived":false,"fork":false,"pushed_at":"2024-04-29T11:16:49.000Z","size":620,"stargazers_count":67,"open_issues_count":0,"forks_count":2,"subscribers_count":1,"default_branch":"main","last_synced_at":"2024-05-01T15:25:55.266Z","etag":null,"topics":["esm","hmr","nodejs"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Julien-R44.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-03-27T01:05:17.000Z","updated_at":"2024-05-03T07:55:15.463Z","dependencies_parsed_at":"2024-05-03T07:54:31.287Z","dependency_job_id":"a19a40ee-b893-485e-a72a-aeb7db6f6dad","html_url":"https://github.com/Julien-R44/hot-hook","commit_stats":null,"previous_names":["julien-r44/hot-hook"],"tags_count":39,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Julien-R44%2Fhot-hook","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Julien-R44%2Fhot-hook/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Julien-R44%2Fhot-hook/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Julien-R44%2Fhot-hook/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Julien-R44","download_url":"https://codeload.github.com/Julien-R44/hot-hook/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252768263,"owners_count":21801366,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["esm","hmr","nodejs"],"created_at":"2024-10-26T05:42:48.508Z","updated_at":"2025-05-06T20:41:39.490Z","avatar_url":"https://github.com/Julien-R44.png","language":"TypeScript","funding_links":["https://github.com/sponsors/Julien-R44/"],"categories":[],"sub_categories":[],"readme":"# Hot Hook\n\nHot Hook is a simple and lightweight library for adding hot module replacement in NodeJS with ESM.\n\nYou know how in frameworks like React or VueJS, you edit a file and the page updates automatically without needing to refresh? Well, it's the same concept but for NodeJS.\n\nTake an Express server, for example. The most common development process involves watching the entire project with tools like nodemon and restarting the whole server whenever a file changes. With Hot Hook, you no longer need to restart the entire server; you can make it so only the changed module/file is reloaded. This provides a much faster DX and feedback loop.\n\nThe library is designed to be very light and simple. It doesn't perform any dark magic, no AST parsing, no code transformation, no bundling. It just reloads the changed module. \n\n## Installation\n\n\u003e [!TIP]\n\u003e If you're using AdonisJS, Hono, or Fastify, we have examples in the examples folder to help you set up Hot Hook in your application.\n\n```bash\npnpm add hot-hook\n```\n\n### Initialization\n\nYou have two ways to initialize Hot Hook in your application.\n\n### Using `--import` flag\n\nYou can use the `--import` flag to load the Hot Hook hook at application startup without needing to use `hot.init` in your codebase. If you are using a loader to transpile to TS (`ts-node` or `tsx`), Hot Hook must be placed in the second position, after the TS loader otherwise it won't work.\n\n```bash\nnode --import=tsx --import=hot-hook/register ./src/index.ts\n```\n\nTo configure boundaries and other files, you'll need to use your application's `package.json` file, in the `hot-hook` key. For example: \n\n```json\n// package.json\n{\n  \"hotHook\": {\n    \"boundaries\": [\n      \"./src/controllers/**/*.tsx\"\n    ]\n  }\n}\n```\n\nNote that glob patterns are resolved from the `package.json` directory.\n\nOr you can still use the `import.meta.hot?.boundary` attribute in your code to specify which files should be hot reloadable.\n\n### Using `hot.init`\n\nYou need to add the following code as early as possible in your NodeJS application.\n\n```ts\nimport { hot } from 'hot-hook'\n\nawait hot.init({\n  root: import.meta.filename,\n  // options\n})\n```\n\nThe `hot.init` function internally call [`register`](https://nodejs.org/api/module.html#moduleregisterspecifier-parenturl-options) from `node:module` to hook into the module loading process. This is why you need to call it as early as possible, otherwise modules imported before the call to `hot.init` will not benefit from hot module replacement.\n\nNext, you need to include the types for `import.meta.hot` in your project. To do this, add the following code in a `.d.ts` file or in the `types` property of your `tsconfig.json`.\n\n```ts\n/// \u003creference types=\"hot-hook/import-meta\" /\u003e\n```\n\n```json\n{\n  \"compilerOptions\": {\n    \"types\": [\"hot-hook/import-meta\"]\n  }\n}\n```\n\n## Usage\n\nOnce Hot Hook is initialized in your application. Every time you want HMR working for a specific module and their dependencies, you need to dynamic import (`await import`) the module and make sure this `await import` is called often enough to reload the module when needed.\n\nIn the case of an HTTP server, you would typically dynamic import your controller or route handler module. So every time a request is made, Hot Hook will be able to reload the module (and its dependencies) if it has changed. \n\nAlso note that you must use `import.meta.hot?.boundary` when importing the module. This is a special [import](https://nodejs.org/api/esm.html#import-attributes) attributes that allows to create what we call an [HMR boundary](#boundary).\n\n\u003e [!TIP]\n\u003e If using `import.meta.hot?.boundary` is not of your taste, you can also hardcode the list of files that you want to be hot reloadable using glob patterns in the [`boundaries` options of `hot.init`](#boundaries)\n\nExample :\n\n```ts\nimport * as http from 'http'\n\nconst server = http.createServer(async (request, response) =\u003e {\n  const app = await import('./app.js', import.meta.hot?.boundary)\n  app.default(request, response)\n})\n\nserver.listen(8080)\n```\n\nThis is a simple example, the `app.js` module will always be reloaded with the latest version every time you modify the file and make a new request. However, the http server will not be restarted.\n\nWe have some examples in the examples folder with different frameworks to help you set up Hot Hook in your application. If you are using [AdonisJS](https://adonisjs.com/): it's your lucky day. Hot hook was the reason why I created this library and we gonna have a complete integration with AdonisJS soon.\n\n## Options\n\n`hot.init` accepts the following options:\n\n\n### `root`\n\nThe path of the root file. This is the entry point of your application. Glob patterns are resolved from the directory of this file.\n\n```ts\nawait hot.init({\n  root: import.meta.filename\n})\n```\n\n### `ignore`\n\nAn array of glob patterns that specifies which files should not be considered by Hot Hook. That means they won't be reloaded when modified. By default, it's `['node_modules/**']`.\n\n### `boundaries`\n\nAn array of glob patterns that specifies which files should be considered as HMR boundaries. This is useful when you don't want to use `import.meta.hot?.boundary` in your code.\n\n```ts\nawait hot.init({\n  boundaries: [\n    './app/**/controllers/*.ts'\n  ]\n})\n```\n\n## API\n\n### import.meta.hot\n\nThe `import.meta.hot` variable is available if you need to condition code based on whether hot-hook is enabled or not.\n\n```ts\nif (import.meta.hot) {\n  // Specific code that will use import.meta.hot\n}\n```\n\nOr simply use optional chaining:\n\n```ts\nimport.meta.hot?.dispose()\n```\n\n### import.meta.hot.dispose()\n\n`import.meta.hot.dispose` is a function that allows you to specify code that should run before a module is reloaded. This can be useful for closing connections, cleaning up resources, etc.\n\n```ts\nconst interval = setInterval(() =\u003e {\n  console.log('Hello')\n}, 1000)\n\nimport.meta.hot?.dispose(() =\u003e {\n  clearInterval(interval)\n})\n```\n\nHere, each time the module is reloaded, the `interval` will be cleaned up.\n\n### import.meta.hot.decline()\n\n`import.meta.hot.decline` is a function that allows you to specify that the module should not be reloaded. This can be useful for modules that are not supposed to be hot reloaded, like configuration files.\n\n```ts\nimport.meta.hot?.decline()\n\nexport const config = {\n  port: 8080\n}\n```\n\nIf this file is modified, then hot hook will call the `onFullReloadAsked` function, which you can specify in the options of `hot.init`. Otherwise, by default it will just send a message to the parent process to reload the module.\n\n## How it works ?\n\nFirst, let's start by explaining the basics.\n\n### What is a hook ? \n\nHot Hook is a [hook](https://nodejs.org/api/module.html#customization-hooks) for Node.js. In short: a hook is a way to intercept the loading of a module. Every time you do an import in your code, Hot Hook can intercept this and perform additional actions like injecting or transforming the module's imported code, recording information about the module, etc.\n\n### ESM Cache busting\n\nOnce you use an import, Node.js loads the module into memory and keeps it in cache. This means that if you import the same module multiple times in your application, Node.js will load it only once throughout the application's lifetime.\n\nThis is problematic for hot module replacement.\n\nPreviously, with CommonJS (require), we had control over this Node.js cache. We could remove a module from the cache (`delete require.cache`), and thus a require on this module would force Node.js to fetch the latest version of the module.\n\nSo, how do we do this in ESM? There have been lots of discussions on this topic for a while (https://github.com/nodejs/node/issues/49442, https://github.com/nodejs/help/issues/2806). But for now, there's no official solution. However, there is a trick. A trick that causes memory leaks, but they are so minimal that it shouldn't be a problem for most applications. Especially since we use this trick ONLY in development mode.\n\nThis trick is what Hot Hook uses to do hot module replacement. And it simply involves adding a query parameter to the URL of the imported module. This forces Node.js to load again the module, thus getting the latest version of the module.\n\n```ts\nawait import('./app.js?v=1')\nawait sleep(5_000)\nawait import('./app.js?v=2')\n```\n\nIf you execute this code, and modify the app.js file between the two imports, then the second import will load the latest version of the module you've saved.\n\n### Boundary\n\n\"HMR boundaries\" are an important concept in Hot Hook. The so-called \"boundary modules\" are modules that are marked as being hot reloadable using the `import.meta.hot?.boundary` attribute during their importation (or using the `boundaries` configuration in your `package.json`) :\n\n```ts\nawait import('./users_controller.js', import.meta.hot?.boundary)\n```\n\n\u003e [!TIP]\n\u003e One important thing to note is, ONLY dynamic imports can be hot reloadable. Static imports will not be hot reloadable so don't declare them as boundaries. Read more about this [here](#esm-cache-busting).\n\nBy importing a module this way, you are essentially creating a kind of boundary. This module and all the modules imported by it will be hot reloadable.\n\nLet's take a more complete example. Essentially, Hot Hook has a very simple algorithm to determine whether the file you just edited is hot reloadable or not.\n\n- Starting from the modified file, Hot Hook will go up the whole dependency tree until it can reach the root file (the entry point of the application/the executed script).\n- If Hot Hook can reach the root file without encountering any boundary file, then it means we need to do a full reload of the server.\n- If all paths to reach the root file go through boundary files, then it means we can hot reload the modified file.\n\nExample with this typical tree diagram of an HTTP application:\n\n![Diagram](./assets/diagram.png)\n\nIn this example, `users_controller.ts` and `posts_controller.ts` are boundary files. If you modify one of these two files, then Hot Hook can hot reload them. Now let's consider other cases.\n\n- `app/models/posts.ts`. It is hot reloadable, because the only path to reach the root file goes through `posts_controller.ts`, which is a boundary file.\n- The same goes for `app/services/post.ts`.\n- `utils/helpers.ts` is also hot reloadable because the only path to reach the root file goes through `users_controller.ts`, which is a boundary file.\n- Now, more interestingly, `app/models/user.ts` is NOT hot reloadable. Because there are TWO paths to reach the root file. The first goes through `users_controller.ts`, which is a boundary file, but the second goes through `providers/database.ts`, which is not a boundary file. Therefore, Hot Hook cannot hot reload `app/models/user.ts`. A modification to this file would require a full reload of the server. If `providers/database.ts` did not import `app/models/user.ts`, then `app/models/user.ts` would be hot reloadable.\n\n### Full reload\n\nNow, how do we perform a full reload? How do we force Node.js to reload the entire process?\n\nFor this, there's no secret : you will need a process manager. Whenever a file that should trigger a full reload is updated, Hot Hook will send a message to the parent process to tell it to reload the module. But for that, you need a parent process. And a parent process that understands this instruction.\n\nSo the concept is simple: the manager needs to launch your application as a child process and listen to messages from the child process. If the child process sends a message asking for a full reload, then the manager must kill the child process and restart it.\n\nIt's quite simple. However, we ship a process manager with Hot Hook. See the documentation of the runner [here](./packages/runner/) for more information, and also see the examples in the [examples](./examples/) folder that use the runner.\n\n### Hot Hook\n\nWith all that, Hot Hook is ultimately quite simple:\n\n- Intercept imports with a hook\n- Collect all imported files and build a dependency tree\n- If a file changes, check if it is hot reloadable by applying the algorithm described [above](#boundary)\n- If it is hot reloadable, then add a `version` query parameter to the URL of the file\n- Thus, the next time the module is imported, Node.js will load the latest version of the module\n\nSimple, lightweight, and efficient.\n\n## Dump\n\nIf you need to retrieve Hot Hook's dependency graph then you can access it as follows: \n\n```ts\nimport { hot } from 'hot-hook'\nconsole.log(await hot.dump()) \n\n/**\n * Output will be something like this\n */\nconst dump = [\n   {\n      \"path\":\"server.ts\",\n      \"boundary\": false,\n      \"reloadable\":false,\n      \"dependencies\":[\"../node_modules/@adonisjs/core/build/index.js\",\"../start/env.ts\"],\n      \"dependents\":[]\n   },\n   {\n      \"path\":\"../node_modules/@adonisjs/core/build/index.js\",\n      \"boundary\":false,\n      \"reloadable\":false,\n      \"dependencies\":[\"../node_modules/@adonisjs/core/build/src/config_provider.js\"],\n      \"dependents\":[\"server.ts\", \"../app/pages/controllers/landing_controller.tsx\", \"../app/auth/controllers/login_controller.tsx\", \"../app/auth/services/auth_service.ts\"]\n   },\n]\n```\n\n### Viewer\n\nIf you need to visualize the same dependency graph, you can use Hot Hook's Dump viewer. First make sure to install the `@hot-hook/dump-viewer` package and add the following code to your application:\n\n```ts\nrouter.get('/dump-viewer', async (request, reply) =\u003e {\n  const { dumpViewer } = await import('@hot-hook/dump-viewer')\n\n  reply.header('Content-Type', 'text/html; charset=utf-8')\n  return dumpViewer()\n})\n```\n\nThen access your server's `/dump-viewer` URL to view the graph: \n\n![dump viewer](./assets/dump_viewer.png)\n\n## Alternatives\n\nIf you are looking for a more complete HMR solution, you can take a look at [dynohot](https://github.com/braidnetworks/dynohot)\n\n## Sponsors\n\nIf you like this project, [please consider supporting it by sponsoring it](https://github.com/sponsors/Julien-R44/). It will help a lot to maintain and improve it. Thanks a lot !\n\n![](https://github.com/julien-r44/static/blob/main/sponsorkit/sponsors.png?raw=true)\n\n## Credits\n\nHot Hook was initially forked from [hot-esm](https://github.com/vinsonchuong/hot-esm) by Vinson Chuong. Thanks a lot for the initial work!\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjulien-r44%2Fhot-hook","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjulien-r44%2Fhot-hook","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjulien-r44%2Fhot-hook/lists"}