feat(cva): add cva/tools entry point with getSchema and pva - #383
feat(cva): add cva/tools entry point with getSchema and pva#383joe-bell wants to merge 3 commits into
Conversation
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
cva | 6eec423 | Commit Preview URL Branch Preview URL |
Aug 11 2026, 02:49 PM |
ca139f1 to
bab9c9e
Compare
|
Follow-up idea: build an example that showcases using Tailwind's Generated by Claude Code |
bab9c9e to
c03c1b3
Compare
BenchmarksComparing this PR's local benchmark run against the latest published npm versions. Aim for higher ops/s. Treat deltas within ±5% as noise.
|
| Task | This PR | 0.7.1 |
Δ |
|---|---|---|---|
cva (runtime)component call with defaults |
600,841 ops/s ±0.19% | 585,272 ops/s ±4.97% | +2.7% |
cva (runtime)component call with props |
98,774 ops/s ±0.18% | 96,752 ops/s ±0.60% | +2.1% |
cx (runtime)class join |
5,602,270 ops/s ±0.19% | 5,705,678 ops/s ±0.47% | -1.8% |
cva (static)component definition |
13,876,496 ops/s ±0.08% | 14,096,242 ops/s ±0.34% | -1.6% |
compose (static + runtime)define, join and call |
503,594 ops/s ±0.18% | 510,415 ops/s ±0.16% | -1.3% |
cva
| Task | This PR | 1.0.0-beta.8 (beta) |
Δ |
|---|---|---|---|
cva (runtime)component call with defaults |
610,358 ops/s ±0.15% | 609,591 ops/s ±0.18% | +0.1% |
cva (runtime)component call with props |
97,600 ops/s ±0.16% | 95,343 ops/s ±0.22% | +2.4% |
cx (runtime)class join |
5,414,813 ops/s ±0.10% | 4,703,719 ops/s ±0.12% | 🟢 +15.1% |
cva (static)component definition |
1,954,490 ops/s ±0.43% | 1,847,435 ops/s ±0.18% | 🟢 +5.8% |
composes property (static + runtime)define, join and call |
250,306 ops/s ±0.16% | 250,936 ops/s ±0.17% | -0.3% |
Commit 363a098 · Node v24.18.0 · linux x64 · 2026-08-11T14:48:53.553Z
These benchmarks ran in CI on this PR's code. The comment checks that the report looks valid before posting it, but it cannot guarantee the numbers are correct. If you see a large change, re-run the benchmarks locally before treating it as a real improvement or regression.
c03c1b3 to
425d1f7
Compare
425d1f7 to
61fbf32
Compare
Add a second package entry point, cva/tools, for utilities that
introspect the config cva attaches to every component:
- pva (Prop Variant Authority) resolves a component's class string and
its variant state, as data-* attributes (e.g. data-intent="primary"),
from a single props object, returning { class, className, data }: the
class string under both framework prop names, and the attributes
nested under data. Resolution falls back to defaultVariants exactly
like class resolution; attribute names are kebab-cased variant names;
values are always stringified. A WeakMap keyed by component caches the
kebab-cased names and stringified defaults, so per-render calls skip
the regex work. Closes JB-179.
- getSchema moves here as its canonical home; the package root keeps a
deprecated re-export for backwards compatibility.
The build side rides on the tsdown pipeline from #385: the new entry is
one line in tsdown.config.mts, the exports/publishConfig.exports maps
are build-regenerated, and node10 subpath types resolve via a
hand-added publishConfig.typesVersions mapping (tsdown preserves it;
same pattern as class-variance-authority's ./types). Rolldown emits the
root's deprecated re-export as an entry-to-entry import (index.mjs
imports ./tools.mjs), so getSchema stays one instance across both entry
points. size-limit gains a dist/tools.cjs entry (527 B, capped 0.6 kB);
dist/index.cjs stays within its 1.6 kB cap.
Docs: the beta "Utilities" page becomes "Tools" (redirect included),
with pva documented there, in the API reference, in What's New, and
replacing the hand-written data attributes in the polymorphism example.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W5Qfb2yGnGnTZReRKSt6hr
pva's data attributes resolved props with `prop === undefined` fallback
while class resolution uses `falsyToString(prop) || falsyToString(default)`,
so an (untyped) null or empty-string prop produced an element carrying the
default's classes but announcing data-intent="null" - a data-[intent=primary]
selector would miss the very element styled as primary. The data pass now
shares falsyToString's exact semantics (lockstep comment on both sides), so
data always reports the variant key class resolution selected; a parity test
suite pins null, "", false, 0, and negative values.
While reworking the resolution loop, reshape the per-component cache from a
tuple array to { defaults, byKey }: defaults is a spreadable object of
resolved default attributes, byKey maps variant names to attribute names, and
each call spreads defaults then overrides from the (typically fewer) own
props keys. Benchmarked against the previous shape at 1433 -> 1196 ns/op per
pva call (5-variant component, 2M iterations). byKey has a null prototype so
an own __proto__ key in JSON-sourced props can never resolve to a junk
attribute (pinned by test). Attribute insertion order changes (defaults
first, no-default variants after) - order carries no meaning for DOM
attributes and tests compare order-insensitively.
Also: document the immutability invariant the cache relies on at the
component.config assignment in index.ts, add JSDoc (with examples) to the
exported getSchema and pva, pin the spec-correct data--tone encoding of
leading-uppercase variant names (dataset.Tone round-trip), and advise
module-scope component definitions in the tools docs. dist/tools.cjs grew
527 B -> 598 B with the parity logic, so its size-limit cap moves 0.6KB ->
0.7KB (dist/index.cjs unchanged under 1.6KB).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W5Qfb2yGnGnTZReRKSt6hr
Adapt cva/tools to the internal variants feature from #382: a _-prefixed variant is hidden from introspection, so pva's data output now omits it the same way getSchema does - no default attribute is precomputed and no byKey entry exists for a prop to emit one, at runtime and in CVADataAttributes' type. Its classes still resolve normally. Documented in the tools page, API reference, and the What's New internal-variants section. Also restore two getSchema coverage tests (defaulted variant with no values; variant with neither values nor default) and the plain-function runtime assertion that previously lived in index.test.ts's getSchema block, which this branch relocates to tools.test.ts - keeping the enforced 100% coverage thresholds green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W5Qfb2yGnGnTZReRKSt6hr
025f9f4 to
6eec423
Compare
Description
Implements JB-179: lets consumers derive
data-*attributes from the samecvacomponent that produces class names.Adds a second package entry point,
cva/tools, for utilities that introspect the configcvaattaches to every component. It exports two functions:getSchema(moved from the package root, which keeps a deprecated re-export) andpva.pva(component, props?)(Prop Variant Authority: glue that merges your props) resolves a component's class string and its variant state, asdata-*attributes, from a single props object:The class string is returned under both
classandclassName(so React, Vue, Svelte, and Astro can each destructure their native prop name), withdatanested rather than spread at the root, so there's no ambiguity about which key holds the class string. Attribute names are kebab-cased variant names; values are always stringified.Parity guarantee: the data attributes always report exactly the variant values class resolution selected — both sides share
falsyToString's fallback semantics (lockstep comments intools.ts/index.ts), so even untyped edge cases (null/""props falling back to defaults,false/0resolving as themselves) can't produce an element whose classes anddata-*disagree. Pinned by a dedicated parity test suite.Internal variants (#382) are omitted from
pva'sdataoutput the same waygetSchemaomits them — no attribute at runtime, no key in the type — while their classes still resolve normally.Performance: per-component work (kebab-casing, default stringification) is cached in a
WeakMapkeyed by component identity as a spreadable defaults object plus a null-prototype variant-to-attribute lookup (which also makes an own__proto__key in JSON-sourced props harmless); each call spreads the defaults and overrides from the props keys. Benchmarked at 1433 → 1196 ns/op perpvacall versus the previous loop shape (5-variant component). The cache relies oncomponent.configbeing construction-time immutable, now documented at its assignment site.getDataAttributes, the lower-level primitivepvais built on, stays internal —pvacovers the common case, andpva(component, props).data(ignoring.class/.className) covers the rest (e.g. attributes on a different element than the class).Build: rides the tsdown pipeline from #385 — the new entry point is one line in
tsdown.config.mts; theexports/publishConfig.exportsmaps are build-regenerated.node10subpath types resolve via a hand-addedpublishConfig.typesVersionsmapping (tsdown preserves it; same pattern asclass-variance-authority's./types). Rolldown emits the root's deprecatedgetSchemare-export as an entry-to-entry import (index.mjsimports./tools.mjs), so it stays one instance across both entry points.size-limitgains adist/tools.cjsentry (620 B, capped at 0.7 kB);dist/index.cjsstays within its existing 1.6 kB cap.Docs: the beta "Utilities" page becomes "Tools" (redirect included), with
pvadocumented there (including a note to define components at module scope so the per-component cache is effective), in the API reference, in What's New, and replacing the hand-written data attributes in the polymorphism example. The exportedpva/getSchemaalso carry JSDoc@exampleblocks for editor hover docs.Additional context
Marked draft for review before merge. Rebased onto latest
main(internal variants #382, 100% coverage enforcement #390,cva@1.0.0-beta.8). Verification performed: full test suite (350 tests, meeting the enforced 100% statement/branch/function/line coverage;tools.test.tscovers defaults/overrides, booleans, negatives, kebab-casing incl. the spec-correctdata--toneencoding of leading-uppercase names, composed components, cache consistency, class-resolution parity fornull/""/false/0,__proto__-key safety, internal-variant omission, and type-level rejections),check:tsc, the build's built-in attw (strict, no problems across all resolution modes for both entry points) + publint + unused-dependency gates,bundlesize, docs build, and the packed artifact consumed from a fixture withtsc --strictunder bothnodenextandbundlerresolution plus CJS/ESM runtime smokes. Security review pass applied to the diff; no findings.What is the purpose of this pull request?
Before submitting the PR, please make sure you do the following
fixes #123).