Skip to content

Architecture

Layout

src/
├── index.ts              # public entry: createHabitus() + re-exports
├── components/
│   ├── index.ts          # barrel — one line per component directory
│   └── Button/
│       ├── HabButton.vue     # the component
│       ├── HabButton.spec.ts # colocated spec
│       ├── types.ts          # props/variant types, re-exported publicly
│       └── index.ts          # the directory's only public surface
├── composables/          # shared reactive logic
├── utils/                # framework-agnostic helpers
└── styles/
    ├── theme.css         # tokens -> ships as /theme
    └── index.css         # Tailwind entry -> compiles to /styles

One directory per component, containing everything about that component. A spec lives next to the code it tests because that keeps the whole unit of work in one place.

Rules that are enforced

These are lint errors, not conventions:

  • A component directory's index.ts is its only public surface. Importing ../Button/HabButton.vue from another component is an error; import ../Button. Reaching past the index couples two components to each other's internals so neither can be refactored safely.
  • No @/* alias imports inside src/. vue-tsc copies import specifiers verbatim into the emitted .d.ts, so an aliased import ships as a literal @/utils/cx that no consumer can resolve. Relative imports only. The alias exists for docs/, which is never published.
  • Props and emits are declared with types, not runtime objects.vue-component-meta reads types to generate the API tables; a runtime-object declaration produces an empty table rather than an error, which is exactly the kind of silent failure that rots documentation.
  • No any.
  • No circular imports.

Rules that are checked in CI

npm run check:docs fails when a component lacks a docs page, when that page does not include its generated API table, or when the component is not exported from the barrel.

Design constraints

The JS entry never imports CSS. src/index.ts importing the stylesheet would make the whole package one unconditional side effect and defeat tree-shaking. Consumers import @livereader/habitus-vue/styles explicitly. sideEffects in package.json is scoped to **/*.css accordingly.

ESM only. Every consumer is a Vite + Vue 3 app. A UMD/CJS build would double the output and the bug surface for a consumer that does not exist.

Per-module output. The build runs Rollup with preserveModules, mirroring src/ into dist/ one file per module, so importing one component does not pull in the rest. Note preserveEntrySignatures: "strict" is required: Vite only sets it for build.lib, and without it Rollup treats every public export as unused and emits empty files — a build that passes and ships nothing.

vue and reka-ui stay external. A second copy of reka-ui would break its provide/inject context sharing.

The CSS subpaths carry a types condition. TypeScript 6 rejects a side-effect import that resolves through exports without declarations (TS2882), so ./styles and ./theme both point their types at dist/css.d.ts, an empty module that exists only to be found.

Verifying the published surface

npm run check:exports runs are-the-types-wrong over a packed tarball, resolving every subpath the way a consumer would. It runs with --profile esm-only, since node10 and CommonJS failures describe consumers this package deliberately does not serve.

One rule is ignored: internal-resolution-error. The emitted declarations reference ./HabButton.vue, and node16 type resolution cannot follow a .vue specifier — an inherent consequence of shipping Vue SFC types, not something to work around. The bundler resolution mode that every consumer actually uses is clean, and the tarball is verified against a real Vite app.

Testing layers

Present today:

  • Unit and behaviour — vitest + @vue/test-utils on happy-dom.
  • Accessibility — axe-core via tests/a11y.ts. tests/a11y.spec.ts verifies the harness itself still detects violations; if it fails, every a11y assertion in the repo has silently become a no-op.

Intended next, not yet set up:

  • Real-browser interaction@vitest/browser + Playwright, for focus management, keyboard navigation and pointer behaviour that happy-dom cannot model faithfully.
  • Visual regression — screenshot diffing per component variant.

Anything relying on real layout or focus behaviour should wait for the browser layer rather than being approximated in happy-dom.

Proprietary — internal use only.