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
@mittwald/flow-react-components depends on @mittwald/react-tunnel with an exact version equal to its own (1.1.24 → react-tunnel@1.1.24). For a package whose whole job is a React context, an exact regular dependency is the one shape that cannot be deduped by a consumer — and the failure is a runtime throw with no type or lint signal.
What we hit
mStudio depends on @mittwald/react-tunnel directly, because it runs its own tunnels (MStudioTunnelProvider, the frontend-fragment tunnels, a domain-contact form). It was on 0.2.0-alpha.1041 while flow-react-components@1.1.0-next.0 resolved react-tunnel@1.1.0-next.0. pnpm kept both:
Error: Could not get tunnel for provider @mittwald/flow/tunnel/Heading.
Please provider a TunnelProvider with this ID.
The provider is there — flowComponent wraps every ui/layout component in a UiComponentTunnelProvider, and Flow's own TunnelExit inside the same Heading finds it fine. What differs is the module instance: two copies of react-tunnel mean two distinct createContext objects, so the app's TunnelEntry walks a context chain that never contains Flow's provider. TunnelState's lookup then throws.
tsc is happy, ESLint is happy. At runtime the throw took our whole secondary navigation into its error boundary. Aligning our direct dependency to exactly the version Flow resolves fixed it.
Why this is easy to hit and hard to see
The two lines are released in lockstep with identical version numbers, so a ^ range on both looks safe — and dedupes on any day both are published. It splits the first time the numbers diverge, and the symptom appears far from the cause.
For us it also means our direct dependency has to stay pinned without a caret and be moved by hand on every Flow bump, which is exactly the kind of coupling that gets forgotten.
The exact pin itself looks like a publish artifact rather than a decision — react-tunnel lives in this monorepo (packages/react-tunnel), and per #2887workspace:* becomes an exact pin at publish time. Different failure mode than #2887 (this is a regular dependency and the version does exist), same origin.
Options
Rough order of preference, all from the outside — you'll know which fits the monorepo:
Re-export the tunnel API from flow-react-components (e.g. a @mittwald/flow-react-components/tunnel subpath). Consumers then never declare react-tunnel at all and a second instance becomes impossible.
Widen the regular dependency to a range (^1.1.0). Cheapest, but it only helps consumers who also use a caret — a consumer that pins still ends up with two copies.
Related, much smaller
getTunnelProviderId isn't exported, so a consumer that wants to render into a Flow component's tunnel exit has to hardcode the internal prefix "@mittwald/flow/tunnel/Heading". Exporting the helper (or the provider ids as constants) would make that supported instead of a guess. Happy to split this into its own issue if you'd rather keep them separate.
@mittwald/flow-react-componentsdepends on@mittwald/react-tunnelwith an exact version equal to its own (1.1.24→react-tunnel@1.1.24). For a package whose whole job is a React context, an exact regular dependency is the one shape that cannot be deduped by a consumer — and the failure is a runtime throw with no type or lint signal.What we hit
mStudio depends on
@mittwald/react-tunneldirectly, because it runs its own tunnels (MStudioTunnelProvider, the frontend-fragment tunnels, a domain-contact form). It was on0.2.0-alpha.1041whileflow-react-components@1.1.0-next.0resolvedreact-tunnel@1.1.0-next.0. pnpm kept both:We then tried to render app content into a
<Heading>'s own tunnel exit — theheading-contentslot that Flow itself uses forBadgeandAlertBadge:Result:
The provider is there —
flowComponentwraps every ui/layout component in aUiComponentTunnelProvider, and Flow's ownTunnelExitinside the sameHeadingfinds it fine. What differs is the module instance: two copies ofreact-tunnelmean two distinctcreateContextobjects, so the app'sTunnelEntrywalks a context chain that never contains Flow's provider.TunnelState's lookup then throws.tscis happy, ESLint is happy. At runtime the throw took our whole secondary navigation into its error boundary. Aligning our direct dependency to exactly the version Flow resolves fixed it.Why this is easy to hit and hard to see
The two lines are released in lockstep with identical version numbers, so a
^range on both looks safe — and dedupes on any day both are published. It splits the first time the numbers diverge, and the symptom appears far from the cause.For us it also means our direct dependency has to stay pinned without a caret and be moved by hand on every Flow bump, which is exactly the kind of coupling that gets forgotten.
The exact pin itself looks like a publish artifact rather than a decision —
react-tunnellives in this monorepo (packages/react-tunnel), and per #2887workspace:*becomes an exact pin at publish time. Different failure mode than #2887 (this is a regular dependency and the version does exist), same origin.Options
Rough order of preference, all from the outside — you'll know which fits the monorepo:
flow-react-components(e.g. a@mittwald/flow-react-components/tunnelsubpath). Consumers then never declarereact-tunnelat all and a second instance becomes impossible.peerDependencywith a range, so package managers keep one instance and warn on a mismatch instead of silently installing two. Needs a real range, notworkspace:*, for the reason Publish holes: versioned-but-unpublished releases break exact peer pins #2887 documents.^1.1.0). Cheapest, but it only helps consumers who also use a caret — a consumer that pins still ends up with two copies.Related, much smaller
getTunnelProviderIdisn't exported, so a consumer that wants to render into a Flow component's tunnel exit has to hardcode the internal prefix"@mittwald/flow/tunnel/Heading". Exporting the helper (or the provider ids as constants) would make that supported instead of a guess. Happy to split this into its own issue if you'd rather keep them separate.