chrisscho.uk
[ LEARNING ]Aug 2026Architecture and migration

One server-only import breaks every component in the bundle

A component whose data layer imports server-only doesn't fail on its own when bundled for a browser, it throws on import and takes every other component in the bundle with it.

Source: .design-sync/NOTES.md, chrisscho.uk

I export this site's components into a design tool, which means bundling them for a browser outside the Next build that normally wraps them. The first attempt failed on every component at once, and the error pointed at none of them.

What actually happened

One component on this site runs reachability checks against my live properties. Its data layer starts with import "server-only", which is the standard way to make sure server code never reaches a client bundle. The package does that by throwing the moment it is imported in a browser environment.

That is exactly the behaviour I asked for. What I hadn't thought through is the blast radius. The throw happens at import time, not at render time, and the bundle imports everything through a single barrel file. So the failure wasn't scoped to the one component that touched server code. It took out every component in the bundle, including the dozen that had nothing to do with it.

Why it reads as a mystery

A render-time failure names the component that failed. An import-time failure in a shared barrel names the barrel, so the stack trace points at the entry file and the list of things that broke is "all of them". I spent a while looking for something wrong with the bundler configuration because the shape of the error looked like configuration rather than one bad module.

What I do now

The component is excluded from the bundle and the exclusion is written down next to the reason, because "why is this one missing" is a question future me was definitely going to ask. It can never be included while its data layer depends on server-only code, and that is a property of the architecture rather than a bug to fix later.

The general rule I took from it: when you bundle a subset of an app for a different runtime, audit the import graph rather than the component list. A component is only as portable as the deepest module it pulls in, and a barrel file makes every component share the worst case.

The wider point

Guardrails that fail loudly are the right design, and server-only is a good one. Loud failure gets fixed and silent failure hides. The cost is that a loud failure at import time is indiscriminate, so the thing you learn to look at first is not the error but where in the lifecycle it fired.

This is the kind of thing the audit looks for in a business rather than in a codebase. See what it covers →