Skip to content

react-tunnel's exact pin gives consumers a second module instance — and a second React context #3134

Description

@mfal

@mittwald/flow-react-components depends on @mittwald/react-tunnel with an exact version equal to its own (1.1.24react-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:

node_modules/.pnpm/@mittwald+react-tunnel@0.2.0-alpha.1041_…
node_modules/.pnpm/@mittwald+react-tunnel@1.1.0-next.0_…

We then tried to render app content into a <Heading>'s own tunnel exit — the heading-content slot that Flow itself uses for Badge and AlertBadge:

<Heading>
  <ModelTitle model={model} />
  <TunnelEntry id="headingContent" providerId="@mittwald/flow/tunnel/Heading">
    {titleSuffix}
  </TunnelEntry>
</Heading>

Result:

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 #2887 workspace:* 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:

  1. 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.
  2. Make it a peerDependency with a range, so package managers keep one instance and warn on a mismatch instead of silently installing two. Needs a real range, not workspace:*, for the reason Publish holes: versioned-but-unpublished releases break exact peer pins #2887 documents.
  3. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions