npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@remix-run/node-hmr

v0.2.0

Published

Run Node.js applications with Hot Module Reloading

Readme

node-hmr

Run Node.js applications with Hot Module Reloading.

Features

  • HMR Runtime: Provides an import.meta.hot API for modules that can handle hot updates
  • Module Hook Friendly: Use Node's module customization hooks API to automatically insert import.meta.hot usage
  • Restart Fallback: Restarts the child Node process when updates aren't accepted
  • Fetch Proxy Support: Wrap fetch handlers so requests are delayed/retried during server updates/restarts
  • Browser HMR Integration: Optionally hosts browser HMR coordination that survives child restarts

Installation

npm i remix

Usage

Create a development script that starts your app server with HMR enabled, along with any additional Node args, such as the --import flag to provide Node module customization hooks for JSX syntax support and Remix component HMR:

// hmr.ts
import { run } from 'remix/node-hmr'

run('./server.ts', {
  nodeArgs: ['--import', 'remix/node-tsx', '--import', 'remix/ui-hmr/node'],
  watch: {
    ignore: ['**/node_modules/**'],
  },
})

Then run the script with Node:

{
  "scripts": {
    "hmr": "NODE_ENV=development node hmr.ts"
  }
}

Fetch Proxy Support

During development, server updates can briefly leave your app unable to handle requests. In a server-only context, requests may be rejected while the child server is restarting. In a browser context, the browser may refresh or revalidate at the same time as a server restart, which can result in failed requests or a broken page.

A stable proxy server can avoid this by continuing to listen on the public port while node-hmr updates the child server behind it. createHmrReadyFetch() works with any fetch handler, so you can compose it with createFetchProxy() from remix/fetch-proxy to forward requests to the child server while delaying or retrying requests during updates.

// hmr.ts
import * as http from 'node:http'

import { createFetchProxy } from 'remix/fetch-proxy'
import { run, createHmrReadyFetch } from 'remix/node-hmr'
import { createRequestListener } from 'remix/node-fetch-server'

const hmrProxyPort = 44100
const appPort = 44101

const hmrRunner = run('./server.ts', {
  env: {
    ...process.env,
    PORT: String(appPort),
  },
  nodeArgs: ['--import', 'remix/node-tsx'],
})

const proxyFetch = createFetchProxy(`http://127.0.0.1:${appPort}`, {
  xForwardedHeaders: true,
})

const server = http.createServer(createRequestListener(createHmrReadyFetch(hmrRunner, proxyFetch)))

server.listen(hmrProxyPort)

By default, createHmrReadyFetch() retries GET and HEAD requests when the wrapped fetch handler throws or returns a 502, 503, or 504 response, but only if the server updated or restarted while the request was in flight. You can customize this policy with shouldRetry:

let fetchWhenReady = createHmrReadyFetch(hmrRunner, proxyFetch, {
  shouldRetry({ request, response }) {
    if (request.method !== 'GET' && request.method !== 'HEAD') return false

    return response === undefined || [502, 503, 504].includes(response.status)
  },
})

Browser HMR Integration

node-hmr can coordinate browser-facing HMR alongside server HMR. The parent process hosts the browser event stream, tracks files reported by asset servers in the child process, sends matching file events back to the child runtime, and emits the resulting browser updates to connected clients.

This is co-ordinated through the use of a browser HMR channel which can be created within the app server when running in node-hmr via the remix/node-hmr/runtime import:

import { createBrowserHmrChannel } from 'remix/node-hmr/runtime'

let browserHmrChannel = await createBrowserHmrChannel()

The remix/node-hmr/runtime API is only available inside a child process supervised by node-hmr. Importing it outside node-hmr throws. Supervised child processes automatically receive the REMIX_NODE_HMR environment variable which you can check before dynamically importing the runtime API:

if (process.env.REMIX_NODE_HMR) {
  let { createBrowserHmrChannel } = await import('remix/node-hmr/runtime')
  let browserHmrChannel = await createBrowserHmrChannel()
}

A browser HMR channel is scoped to the current child process. It gives browser HMR tooling an EventSource URL, a way to report the files it wants watched, and a way to respond to file changes with browser HMR events.

Browser asset servers can use this API to co-ordinate browser HMR with the server, for example, remix/assets via its hmr option to createAssetServer:

import { createAssetServer } from 'remix/assets'

let isDevelopment = process.env.NODE_ENV === 'development'

let assetServer = createAssetServer({
  basePath: '/assets',
  allowFiles: ['app/routes.ts', 'app/**/public/**'],
  denyFiles: ['app/**/*.test.*'],
  hmr:
    isDevelopment && process.env.REMIX_NODE_HMR
      ? async () => (await import('remix/node-hmr/runtime')).createBrowserHmrChannel()
      : undefined,
  watch: isDevelopment,
})

When node-hmr hot updates or restarts server code in a way that should refresh server-rendered UI, it sends a server:update event to connected clients.

Call emitServerReady() when your app server is ready to receive requests. This lets the parent process delay browser server:update events until a restarted app server has finished listening:

server.listen(port, () => {
  if (process.env.REMIX_NODE_HMR) {
    import('remix/node-hmr/runtime').then((nodeHmr) => nodeHmr.emitServerReady())
  }
})

File Watching

The file system is watched automatically so server source changes can hot update or restart the child process.

You can optionally provide an array of glob patterns to the watch.ignore option.

import { run } from 'remix/node-hmr'

run('./server.ts', {
  nodeArgs: ['--import', 'remix/node-tsx', '--import', 'remix/ui-hmr/node'],
  watch: {
    ignore: ['**/node_modules/**'],
  },
})

You can also configure polling behavior. Polling defaults to true on Windows and false elsewhere:

import { run } from 'remix/node-hmr'

run('./server.ts', {
  nodeArgs: ['--import', 'remix/node-tsx', '--import', 'remix/ui-hmr/node'],
  watch: {
    poll: true,
    pollInterval: 100,
  },
})

import.meta.hot

The import.meta.hot API provided by node-hmr is a small runtime contract for modules that can handle updates without restarting the process. It is primarily intended for transforms like remix/ui-hmr, but it can also be used directly.

To type import.meta.hot, add the HMR types to your TypeScript config:

{
  "compilerOptions": {
    "types": ["remix/node-hmr/types"]
  }
}

HMR accept calls are statically analyzed. Write them directly as import.meta.hot.accept(...). Dependency accepts must use string literals or arrays of string literals; do not alias import.meta.hot or pass dynamically constructed dependency lists.

if (import.meta.hot) {
  import.meta.hot.accept()
}

For consistency with browser HMR environments, node-hmr also implements import.meta.hot.on(...), but no events are fired in server modules.

Accepting updates

Calling accept() makes the current module an HMR boundary. When the module changes, node-hmr evaluates the updated module and calls your callback with its exports.

export let value = 1

if (import.meta.hot) {
  import.meta.hot.accept((module) => {
    if (typeof module.value !== 'number') {
      import.meta.hot?.invalidate('Updated module no longer exports value')
      return
    }

    value = module.value
  })
}

You can also accept updates from direct dependencies.

import { value } from './value.ts'

let currentValue = value

export function readValue() {
  return currentValue
}

if (import.meta.hot) {
  import.meta.hot.accept('./value.ts', (module) => {
    if (typeof module.value !== 'number') {
      import.meta.hot?.invalidate('Updated dependency no longer exports value')
      return
    }

    currentValue = module.value
  })
}

Multiple dependencies can be accepted at once. The callback receives an array where only the changed dependency is defined.

if (import.meta.hot) {
  import.meta.hot.accept(['./one.ts', './two.ts'], ([oneModule, twoModule]) => {
    // oneModule is defined when ./one.ts changed.
    // twoModule is defined when ./two.ts changed.
  })
}

Cleaning up

Register cleanup that should run before the module is replaced or disposed.

let interval = setInterval(refreshCache, 30_000)

if (import.meta.hot) {
  import.meta.hot.dispose(() => {
    clearInterval(interval)
  })
}

The data object is preserved across updates for the same module. Use it for small pieces of state.

let count = Number(import.meta.hot?.data.count ?? 0)

export function increment() {
  count++
}

if (import.meta.hot) {
  import.meta.hot.dispose((data) => {
    data.count = count
  })
}

Invalidating updates

Call invalidate() inside an accept callback when the update cannot be applied safely. node-hmr falls back to a process restart.

if (import.meta.hot) {
  import.meta.hot.accept((module) => {
    if (typeof module.value !== 'number') {
      import.meta.hot?.invalidate('Updated module no longer exports value')
      return
    }
  })
}

Related Packages

  • assets - Consumes browser HMR channels for coordinating server and browser HMR updates
  • fetch-proxy - Creates fetch handlers for forwarding requests to another server
  • ui-hmr - Provides code transforms and runtime for HMR for Remix UI components

License

See LICENSE