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
In a monorepo, an app consumes an internal workspace package that is symlinked into the app's node_modules and imported through its built output (dist). When that package's files change on disk (a rebuild), the running Metro server does not reflect the change: Fast Refresh doesn't fire, manual reload (r) re-serves the stale bundle, and only killing + restarting the Metro process picks it up.
We understand r doesn't force a filesystem re-crawl by design — the concern is that Metro's watcher/invalidation never registers the change at all, so nothing short of a restart works.
Setup
apps/mobile/ # Metro project root
node_modules/@scope/ui -> ../../../packages/ui # single symlink
packages/ui/
src/... ; dist/components/<name>/index.js # app imports the dist build
package.json exports: "./components/*" -> "./dist/components/*/index.js"
Import resolves (via symlink) to real pathpackages/ui/dist/components/<name>/index.js.
watchFoldersincludespackages/ui(Expo'sgetDefaultConfigauto-adds every workspace pkg — verified).
Symlink resolution works; app runs fine on cold start.
Repro
Start Metro, load app. 2. Rebuildpackages/ui(changesdist/.../index.js; in our casetsdown, batch writes). 3. Fast Refresh and/orr→still runs the old package code. 4. Kill + restart Metro → change appears.
Expected
A change to a file inside a watchFolder — even reached through a node_modules symlink to a workspace package — should invalidate the module and trigger HMR / be served on next reload, without a full restart.
Ruled out
watchFolders (package dir is watched) · Watchman missing (installed/healthy) · symlink resolution (works cold).
Hypotheses for maintainers
Symlink path-identity mismatch— Watchman reports events under a path spelling that doesn't match the graph's registered (canonicalized real) path, so the delta is dropped.
Batch / atomic-rename writesthrough the symlink aren't handled by the since-clock/invalidation path.
Interaction of the two.
Isolation suggestion: with the same setup, do a single plain editor save (no build tool) to a file the app imports. Fails too → pure symlink watch bug; succeeds → the atomic/batch-write-through-symlink path is the trigger. Happy to share a minimal Expo repo.
Workaround
Dev-only resolver.resolveRequest rewriting @scope/* from dist to the absolute packages/ui/src/... path (no node_modules segment) — after which edits hot-reload reliably.
Two notes:
I kept the reportgeneric(@scope/ui,tsdown-agnostic wording) so it's a clean upstream repro, not tied to your package names.
I was deliberatelyhonest about the uncertainty— I labeled the mechanism as hypotheses and included an isolation experiment, because I haven't instrumented Metro internals to confirm which of symlink-identity vs. atomic-writes is the culprit. Maintainers respond better to "here's the repro + what I ruled out + a way to narrow it" than to a confident wrong diagnosis. If you run that single-editor-save experiment, tell me the result and I'll tighten the report to a definitive cause.
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.
-
Environment
| -- | -- metro / metro-resolver | 0.84.3 react-native | 0.86.0 (New Architecture / Fabric) expo / @expo/metro-config | SDK 57.0.4 Package manager | Bun 1.3.14, isolated linker (linker = "isolated") Watchman | 2025.02.17.00 (installed & used) OS | macOS (Apple Silicon)Summary
In a monorepo, an app consumes an internal workspace package that is symlinked into the app's
node_modulesand imported through its built output (dist). When that package's files change on disk (a rebuild), the running Metro server does not reflect the change: Fast Refresh doesn't fire, manual reload (r) re-serves the stale bundle, and only killing + restarting the Metro process picks it up.We understand
rdoesn't force a filesystem re-crawl by design — the concern is that Metro's watcher/invalidation never registers the change at all, so nothing short of a restart works.Setup
packages/ui/dist/components/<name>/index.js.watchFoldersincludespackages/ui(Expo'sgetDefaultConfigauto-adds every workspace pkg — verified).Repro
packages/ui(changesdist/.../index.js; in our casetsdown, batch writes). 3. Fast Refresh and/orr→ still runs the old package code. 4. Kill + restart Metro → change appears.Expected
A change to a file inside a
watchFolder— even reached through anode_modulessymlink to a workspace package — should invalidate the module and trigger HMR / be served on next reload, without a full restart.Ruled out
watchFolders(package dir is watched) · Watchman missing (installed/healthy) · symlink resolution (works cold).Hypotheses for maintainers
Isolation suggestion: with the same setup, do a single plain editor save (no build tool) to a file the app imports. Fails too → pure symlink watch bug; succeeds → the atomic/batch-write-through-symlink path is the trigger. Happy to share a minimal Expo repo.
Workaround
Dev-only
resolver.resolveRequestrewriting@scope/*fromdistto the absolutepackages/ui/src/...path (nonode_modulessegment) — after which edits hot-reload reliably.Two notes:
@scope/ui,tsdown-agnostic wording) so it's a clean upstream repro, not tied to your package names.Beta Was this translation helpful? Give feedback.
All reactions