{"record":{"id":"35fb0ec4e0430a32","repo":"remix-run/remix","slug":"hmr-must-create-a-channel-with-an-onfileevents-fun","errorCode":null,"errorMessage":"hmr must create a channel with an onFileEvents function","messagePattern":"hmr must create a channel with an onFileEvents function","errorType":"validation","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"packages/assets/src/lib/asset-server.ts","lineNumber":1130,"sourceCode":"    }\n\n    setUnsubscribe(channel.onFileEvents(handleFileEvents))\n    return channel\n  })\n}\n\nfunction validateBrowserHmrChannel(channel: unknown): asserts channel is BrowserHmrChannel {\n  if (channel === null || typeof channel !== 'object') {\n    throw new TypeError('hmr must create an object')\n  }\n  if (!('url' in channel) || typeof channel.url !== 'string') {\n    throw new TypeError('hmr must create a channel with a string url')\n  }\n  if (!('close' in channel) || typeof channel.close !== 'function') {\n    throw new TypeError('hmr must create a channel with a close function')\n  }\n  if (!('onFileEvents' in channel) || typeof channel.onFileEvents !== 'function') {\n    throw new TypeError('hmr must create a channel with an onFileEvents function')\n  }\n  if (!('updateWatchedFiles' in channel) || typeof channel.updateWatchedFiles !== 'function') {\n    throw new TypeError('hmr must create a channel with an updateWatchedFiles function')\n  }\n}\n\nfunction normalizeBasePath(basePath: string): string {\n  if (typeof basePath !== 'string') {\n    throw new TypeError('basePath must be a string')\n  }\n\n  return normalizePathname(basePath || '/').replace(/\\/+$/, '') || '/'\n}\n\nfunction normalizeFingerprintOptions(options: {\n  fingerprint: AssetServerOptions['fingerprint']\n  watch: AssetServerOptions['watch']\n}):","sourceCodeStart":1112,"sourceCodeEnd":1148,"githubUrl":"https://github.com/remix-run/remix/blob/9696913134be3a4423513d2775f7b31d6917c049/packages/assets/src/lib/asset-server.ts#L1112-L1148","documentation":"The HMR channel returned by `hmr.createChannel` must expose an `onFileEvents` function so the asset server can subscribe to file change events for hot updates. This error fires when the channel object is missing that method or it is not callable.","triggerScenarios":"`createChannel` returns an object without an `onFileEvents(callback)` method, so the asset server cannot register its file-event listener.","commonSituations":"Custom channel wrappers (e.g. around a bundler watcher or SSE stream) that only implement `url`/`close`; channels ported from older Remix versions; incomplete test doubles.","solutions":["Implement `onFileEvents(callback: (events: FileEvent[]) => void)` on the returned channel, storing the callback and invoking it when files change","Confirm the other required members (`url`, `close`, `updateWatchedFiles`) exist too","Drop the custom `createChannel` if the default HMR channel suffices"],"exampleFix":"// before\ncreateChannel: () => ({ url, close, updateWatchedFiles })\n// after\ncreateChannel: () => ({\n  url,\n  close,\n  updateWatchedFiles,\n  onFileEvents(callback) {\n    watcher.on('change', (file) => callback([{ type: 'update', file }]))\n  },\n})","handlingStrategy":"validation","validationCode":"let channel = createChannel()\nif (typeof channel.onFileEvents !== 'function') throw new Error('channel must implement onFileEvents')","typeGuard":"function hasOnFileEvents(c: unknown): c is { onFileEvents: (cb: (events: unknown[]) => void) => void } {\n  return !!c && typeof (c as any).onFileEvents === 'function'\n}","tryCatchPattern":null,"preventionTips":["Implement the full channel interface in one place (a base class or factory) so all members stay in sync","Run the dev server once with the custom channel in CI to catch contract violations early"],"tags":["hmr","dev-server","file-watching","validation"],"backgroundTag":"interface-contract-validation","analyzedSha":"9696913134be3a4423513d2775f7b31d6917c049","analyzedAt":"2026-08-27T19:55:01.024Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}