The react adapter is bundled with tsup. One line in its build command decides where the type boundary lives:
tsup src/index.ts --format cjs,esm --dts \ --external react \ --external @tanstack/react-query \ --external @pyrpc/client \ --external @pyrpc/types
Four externals, and each one encodes a resolution decision. The two that matter here are @pyrpc/client and @pyrpc/types.
External means: the app resolves it
Marking a package external tells tsup "do not inline this import; leave the specifier in the output". The consuming app's bundler then resolves it. That is the entire trick of the type-boundary architecture: the import { procedureKinds } from '@pyrpc/types' inside the adapter's dist stays a bare specifier, and the app's alias rewrites it to the generated file.
If @pyrpc/types were bundled into the adapter instead, the generated module could never reach the hooks: the code would be inlined at adapter-build time, fixed to whatever the adapter's own node_modules held (the placeholder), and your app's alias would be powerless to redirect it. Externalizing is what makes runtime substitution possible.
The same logic for react and react-query
React and TanStack Query are externals too, but for a different reason: peer-dependency hygiene. Bundling a second copy of React guarantees hook identity bugs and double-instance errors. The app already provides these, the adapter must share the app's single instance. So the build treats them as external for deduplication, and @pyrpc/types as external for substitution. Same mechanism, two distinct goals.
What "external" does not mean
External does not mean "not a dependency". @pyrpc/types sits in the adapter's dependencies (a change v0.12.0 made explicitly), the package manager must install it so the specifier resolves in the default case. External is about where resolution happens at build time; dependencies is about what gets installed at runtime. The adapter needs both: install the placeholder so imports are satisfiable, keep it external so the app's alias can override it.
The chain stays intact
The adapter's dist is also external to @pyrpc/client, the hook layer re-exports the plain client rather than duplicating it. So the resolution chain is a straight line: your app → @pyrpc/react → @pyrpc/types (aliased to your __pyrpc.ts) and @pyrpc/client. Every hop is external, every specifier survives to the app bundler, and every alias has a chance to redirect. The boundary you define in your config reaches the deepest layer of the adapter.

pyRPC