You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Updating an already-loaded plugin cannot take effect without a full process restart, because the one mechanism that can evict a module from Node's caches — cordis-plugin-hmr — deliberately excludes node_modules, which is where every installed plugin lives.
I maintain dsh-market, so this is the path a few thousand users take every time they update anything. Everything below is measured on @deepseek-ai/dsh@0.1.0-rc.7, not inferred.
What happens today
Installing a new plugin hot-mounts fine — the package was never imported, so a fresh import() is genuinely new code. Updating an existing one cannot, and the failure is silent:
disk dshmarket 1.12.2
GET /dsh-market/status version 1.11.3 (boot id unchanged)
The browser half does update, because the host re-serves the client bundle from disk on the next page load. So the UI visibly becomes the new version while the Node half is still the module imported at boot. Users reasonably conclude the restart notice is a false alarm, skip it, and keep running stale server code against a new UI. We just shipped a fix on our side to stop reporting that state as "live", but the underlying restart is not something we can avoid.
Why it cannot be worked around downstream
cordis-plugin-hmr already implements exactly the hard part — evicting both caches and re-importing, with rollback:
with the comment "Skips node: builtins and node_modules to focus on user code." That scoping is right for plugin development. It also means an installed package can never be reached, and there is no option or hook to make an exception for one — so no downstream plugin can opt into it.
Two further things, noted so the picture is complete rather than as asks:
dsh-web-app/cordis.patch.yml ships - id: hmr, disabled: true with # TODO: Re-enable shared HMR for Web after its reload lifecycle is tested.
HMR needs --expose-internals, which is also the cause of the known startup failure on non-nodejs.org builds (Nix, Alpine musl, some arm64). I assume that is part of why it is off for Web, and I am not suggesting it be flipped on by default.
Loader.Entry.update reuses previous.runtime.callback unless the entry's name changes, so the config path cannot substitute either — but HMR re-imports by filename, so that is not an additional obstacle for this request.
What would help
Roughly in order of how much I think you would want it:
An explicit, narrow reload API — something like ctx.loader.reloadPackage(name) that evicts the module graph rooted at one installed package and re-imports it, independent of the file watcher. A package manager is the natural caller: it knows exactly what changed and when, so no watching of node_modules is needed. This is the one I would build against.
An opt-in escape from the node_modules skip — a config key naming packages HMR may traverse into. Cheaper, but it makes the watcher's job larger and inherits the --expose-internals requirement.
Neither, but say so — if the position is that updates require a restart by design, that is a completely reasonable answer, and I will document it as a property of the platform rather than something we are working around. Knowing it is settled is worth more to me than leaving it open.
Happy to prototype (1) as a PR if the shape sounds right — I would rather ask first than send an unsolicited change to a loader this central. And if there is history behind the node_modules skip that I am missing, I would like to hear it before proposing anything.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Summary
Updating an already-loaded plugin cannot take effect without a full process restart, because the one mechanism that can evict a module from Node's caches —
cordis-plugin-hmr— deliberately excludesnode_modules, which is where every installed plugin lives.I maintain dsh-market, so this is the path a few thousand users take every time they update anything. Everything below is measured on
@deepseek-ai/dsh@0.1.0-rc.7, not inferred.What happens today
Installing a new plugin hot-mounts fine — the package was never imported, so a fresh
import()is genuinely new code. Updating an existing one cannot, and the failure is silent:The browser half does update, because the host re-serves the client bundle from disk on the next page load. So the UI visibly becomes the new version while the Node half is still the module imported at boot. Users reasonably conclude the restart notice is a false alarm, skip it, and keep running stale server code against a new UI. We just shipped a fix on our side to stop reporting that state as "live", but the underlying restart is not something we can avoid.
Why it cannot be worked around downstream
cordis-plugin-hmralready implements exactly the hard part — evicting both caches and re-importing, with rollback:The comments there even handle the Node 22/23 vs 24
loadCacheshape difference. The capability is not missing.What stops it applying is one line in
loadDependencies(cordis-plugin-hmr/lib/index.js:51):with the comment "Skips node: builtins and node_modules to focus on user code." That scoping is right for plugin development. It also means an installed package can never be reached, and there is no option or hook to make an exception for one — so no downstream plugin can opt into it.
Two further things, noted so the picture is complete rather than as asks:
dsh-web-app/cordis.patch.ymlships- id: hmr, disabled: truewith# TODO: Re-enable shared HMR for Web after its reload lifecycle is tested.--expose-internals, which is also the cause of the known startup failure on non-nodejs.org builds (Nix, Alpine musl, some arm64). I assume that is part of why it is off for Web, and I am not suggesting it be flipped on by default.Loader.Entry.updatereusesprevious.runtime.callbackunless the entry'snamechanges, so the config path cannot substitute either — but HMR re-imports by filename, so that is not an additional obstacle for this request.What would help
Roughly in order of how much I think you would want it:
ctx.loader.reloadPackage(name)that evicts the module graph rooted at one installed package and re-imports it, independent of the file watcher. A package manager is the natural caller: it knows exactly what changed and when, so no watching ofnode_modulesis needed. This is the one I would build against.node_modulesskip — a config key naming packages HMR may traverse into. Cheaper, but it makes the watcher's job larger and inherits the--expose-internalsrequirement.Happy to prototype (1) as a PR if the shape sounds right — I would rather ask first than send an unsolicited change to a loader this central. And if there is history behind the
node_modulesskip that I am missing, I would like to hear it before proposing anything.All reactions