Vendor Isolation
@apollion-dsi/core consumes external libraries (animation, virtualization,
positioning, form management, tables, dates, etc.) behind its own wrappers.
DS components import only from those wrappers — never from the library
directly.
TL;DR
Every external library lives under
src/vendors/<Lib>/with an interface Apollion controls. Consumers of@apollion-dsi/corenever import from a vendor — only from the public subpaths.
The policy is enforced by an ESLint rule in @apollion-dsi/eslint-config —
crossing core's internal boundary fails lint.
Why?
- Replaceability. Swapping
tippy.js→@floating-ui/reactis a change localized to the wrapper; no DS component needs a rewrite. That's exactly the churn the wrapper exists to absorb. - Deterministic bundle size. The wrapper exports only what the DS uses — the tree-shaker drops the rest.
- A stable API for you. An internal upgrade (
react-window→@tanstack/react-virtual,formik+yup→react-hook-form+zod,react-router-dom→ a polymorphic Link) never becomes a breaking change for the public component's consumer. - No manual discipline required. The ESLint rule breaks the build if someone imports a restricted dependency directly from a component.
Where they live
18 wrappers under packages/core/src/vendors/ — each with its own
README.md documenting what came in, what was left out, when to
re-evaluate:
Culori · DateFns · Deepmerge · EmotionIsPropValid · FastDeepEqual
FloatingUI · LodashDebounce · Nanoid · ObjectHash
ReactContentLoader · ReactDayPicker · ReactHookForm · ReactSelect
StyledComponents · StyledNormalize · TanstackReactTable
TanstackReactVirtual · ZodSince v6.0.0, the FramerMotion wrapper is gone entirely: ADR-027 replaced the DS's animation runtime with CSS-first motion tokens + the
motionpreset prop, so there is nothing left to vendor. A residual JS engine (Motion One, per the vendor's old swap trigger) stays demand-gated — never re-added preemptively.
Since v5.1, the Culori wrapper is the policy's terminal case: it stopped wrapping the external library and now contains a bit-exact own color engine, matching the original culori — which survives only as the parity tests' oracle (a devDependency). No consumer changed a single line: it's exactly the upstream swap the policy exists to allow.
Mental demo — one import line
❌ Straight from the library
import { DayPicker } from 'react-day-picker';
ESLint: error · build red.
✅ Through the wrapper
import { DayPicker } from '../vendors/ReactDayPicker';
ESLint: ok · build green.
What's NOT under a wrapper
- react / react-dom — explicit peer deps (19.2.x). There's no point vendoring the framework's own runtime.
styled-components is under a wrapper (vendors/StyledComponents)
despite being a regular dependency — the facade isolates the global
DefaultTheme type and lays the groundwork for a future swap (zero-runtime
CSS-in-JS).
For you (consumer)
Nothing changes in your code. Vendor isolation is an internal DS
decision — what you import from @apollion-dsi/core/* stays stable and
remains the public extension point.
For DS contributors
When adding a new dependency to core:
- Create
src/vendors/<NewLib>/withindex.ts+README.md. - Export only the surface the DS needs.
- Import from the wrapper inside components.
- Document in the wrapper's README: what came in, what was left out, when to re-evaluate.
The ESLint rule handles the rest.