Packaging and Monorepos
Version alignment, building libraries on Expressive, and the one-copy rule
One core per app
@expressive/mvc holds module state: the context registry, prototype brands, and the JSX host registration all live in the loaded module. Two copies of the core in one application are two disconnected worlds - a <Provider> in one and a State.get() resolving through the other do not see each other. The classic symptom is Could not find X in context from a component that visibly sits under the right Provider.
The packages are shaped to make one copy the default outcome: @expressive/react and @expressive/router declare @expressive/mvc as a regular dependency with a caret range, so installing the adapter brings a core your package manager can dedupe. You install @expressive/react; the core is its concern.
When the symptom above appears anyway, check for duplicates first:
npm ls @expressive/mvc # or: pnpm why @expressive/mvc / bun pm lsMore than one version listed means something pinned or bounded the core apart from the adapter's range. Align the ranges (or remove the direct @expressive/mvc entry and let the adapter carry it) and reinstall.
Building a library on Expressive
A component library or shared package built on Expressive should not bundle it:
- Declare
@expressive/reactas a peerDependency (with a devDependency copy for local development), the same way you already treatreact. - Mark
@expressive/*external in your build, so your dist imports it rather than inlining a private copy - an inlined copy is the two-worlds problem above, shipped on purpose. - Keep the peer range wide (
^on the minor you develop against, pre-1.0) and move it deliberately: pre-1.0 minors are the breaking channel.
Consumers of your library install @expressive/react themselves - the same contract as react itself. What that costs them: the adapter has an irreducible floor of about 7.6 kB min+gzip (measured, budgeted in CI - see Bundle size), paid once per application regardless of how many libraries share it.
State classes your library exports are ordinary exports. Subclassing, Provider-ing, and get()-ing them works across package boundaries because everyone resolves the same core - which is the point of the rules above.
Monorepos
Workspace tooling makes duplicate cores easier to produce: two apps pinning different adapter versions, a hoisting boundary, or a stale lockfile after a version bump. The diagnosis is the same npm ls @expressive/mvc, and the fix is the same alignment - in a workspace, prefer a single version of @expressive/react declared once and shared.
Version skew between the adapter and core does not need managing by hand: the adapter's caret dependency states the compatible window, and within a single resolved copy the pair is always one your package manager accepted.
Module format
All packages ship ESM only ("type": "module") - no require() entry. Bundlers (vite, esbuild, webpack, rollup) and Metro consume this directly. Jest is the notable exception: its default transformIgnorePatterns skips node_modules, so add an allowance for @expressive if your tests import it. Vitest needs nothing.
Next
- Bundle size - measured cost per import shape, and what tree-shakes.
- Getting started - installation and first model.