0.9.1
Patch Changes
- #1028
tv({ extend })keeps the extended resolver's variant and slot names when the extension declares novariantsorslotsof its own. With nothing to infer from, the extension's schema fell back to its string-indexed constraint, so the merged props took any key:button({ sise: "sm" })andVariantProps<typeof button>accepted a misspelt variant, and a slot resolver accepted any slot name, though an unknown variant value was still caught. An extension that only addsbaseorcompoundVariantsis now typed exactly like the resolver it extends. Compounds and defaults no longer take part in inferring the extension's schema, so they cannot widen it either.
0.9.0
Minor Changes
- #976
engines.nodeis now>=24.0.0, up from>=22.12.0, and Node 22 is no longer supported. Node 24.0.0 is the first release with explicit resource management built in (using,await using,DisposableStack,AsyncDisposableStack,SuppressedError) and all of ES2025, so the packages use both as the platform ships them instead of shimming them for an older line, and the CI matrix runs the unit suite on 24.0.0 itself. Move to Node 24, or stay on the current minor while a deployment still runs Node 22.
Patch Changes
#977 The README states the browser floor: Chrome and Edge 136, Firefox 136, and Safari 18.4 or later, the first releases that ship every ES2025 builtin.
@codefast/di's README also says what a browser program without explicit resource management does: it keepsskipLibCheckon and callsdispose()instead ofawait using.#965
README.mdnow states TypeScript 7 or later as the floor for the package's types — the one compiler every@codefast/*package is built and checked with. No code or declaration changed.
0.8.1
Patch Changes
#892
4c97c5bThanks @thevuong! - Lower the monorepo's Node floor from 24 to 22.12, so the packages install and run on the active Node 22 LTS line.engines.nodebecomes>=22.12.0across every package — the floor the shared toolchain (oxlint, Vite, Vitest, TanStack Start) already requires. Development stays on the latest Node (.node-version) for speed, and a CI matrix exercises the floor and the active LTS directly, so the floor is a contract CI proves rather than one everyone has to run.@types/nodeis pinned to the floor's major (^22), with a workspace override holding the whole tree there so a dev tool's@types/node: "*"peer can no longer pull a newer major and mask an API the floor lacks. The floor stays mechanical, not advisory:@codefast/dikeeps its ownMapupsert helpers rather than the ES2025Map.prototype.getOrInsert(which would raise the floor to 26) and itslibstaysES2024.@codefast/cli's mirror step now calls thenode:pathfunctions directly instead of aliasing them, which the floor's types correctly flag as unbound methods.The shared
@codefast/typescript-configpresets pinlibandtargettoES2024(wasESNext) so the compiler's ECMAScript surface matches the Node floor: an ES2025 builtin such asMap.prototype.getOrInsertnow fails to type-check rather than compiling and crashing on Node 22.12.@codefast/diand@codefast/di-testingalready pinnedliband are unchanged.Internal subpath imports move from a
#/prefix to a bare#(#core/token, not#/core/token), and thepackage.json#importskeys become#*/#tests/*/#examples/*to match. Node's native ESM resolver rejects a#/-prefixed specifier withERR_INVALID_MODULE_SPECIFIERon the whole Node 22 line (and on Node 24 before 24.14), and each package ships those specifiers verbatim inside its publisheddist/*.jsfor a consumer's Node to resolve — so this rename is what actually lets the packages import on the new floor. Purely internal: a consumer's own import paths are unchanged.
0.8.0
Minor Changes
#824
98fc6c7Thanks @thevuong! -tv()no longer compiles at definition. The first call resolves straight from the configuration through a cold lane that answers exactly what the compiled plan answers, and the second call compiles the plan, so defining a component and rendering it once both cost less than upstream while every render after keeps its speed. Two visible consequences:configis a plain property rather than a defined read-only one, and a slot component returns the same slot object for a repeated selection from the second call on.The second call is cheaper too: a flat-lane variant group whose values are all strings is read in place rather than copied onto a prototype-less object, behind the type check the flat lane already makes. Resolvers expose
variantKeys, the variant names in declaration order, as upstream does.For editors,
VariantPropsis now read off the resolver's call signature instead of re-derived from its schema, which makes it far cheaper for the checker on large configurations, and every configuration field and option carries a doc comment that shows on hover.
0.7.1
Patch Changes
#791
37a212bThanks @thevuong! - Point the README's license badge and "License" section at the package's ownLICENSEfile instead of the monorepo root.#793
83cceebThanks @thevuong! - Make "MIT" in the README's License section the link to the package'sLICENSEfile.#796
ba04d27Thanks @thevuong! - Rewrite the README around one structure — what the package is, installation and requirements, a complete quick start, the concepts in learning order, then the documents and the license — with every API claim checked against the current code and no figures that go stale.#784
ad2f93aThanks @thevuong! - Ship the MITLICENSEfile in the published package.filesalready listed it, but the file was missing from the package directory, so the tarball had none.#809
6cd6a0eThanks @thevuong! -VariantSelection— the type a variant function accepts at its call site — now admits an explicitundefinedon every field, so a consumer underexactOptionalPropertyTypescan forward a possibly-undefined prop (variant={variant}) without first omitting it.VariantValues, whichdefaultVariantsdeclares, stays strict. Types only — no runtime change.
0.7.0
0.6.2
0.6.1
0.6.0
Minor Changes
#700
ea48ae2Thanks @thevuong! - Authoring a configuration is now type-checked as strictly as calling one. Three typos used to compile and then do nothing: adefaultVariantskey naming no variant, acompoundVariantskey naming no variant, and a variant class map naming no slot. Each failed the same silent way — nothing read the stray key, the compound never matched, the slot map was dropped — so a mistyped character removed a style with no error anywhere.The root cause was one overload.
extendwas optional onExtendedVariantConfig, which made the last overload a catch-all: a configuration the earlier three correctly rejected still matched it, andTBasewith nothing to infer from widened toVariantSchema, whose key isstring— so every mistyped name became legal again.extendis now required there, which is what that overload was always for. Alongside it,defaultVariantsandcompoundVariantsno longer act as inference sites for the variant schema (NoInfer), so a stray key is rejected instead of quietly widening the schema to include it.Two smaller corrections fall out.
defaultVariantsis typed by the newVariantValues<T>rather than the call-siteVariantSelection<T>, so it no longer accepts aclassNamea configuration has no use for. And in a slot configuration a variant's object value is now held to the declared slots —SlotClassValue<S>— because resolution has always read an object there as slot names rather than clsx conditions;basestays admissible whether or not it is declared, matching the plan that synthesises it.A configuration without slots keeps its clsx object values, and a compound condition naming an undeclared variant still resolves at runtime for JavaScript callers and merged configurations — it is only the typed authoring path that now rejects it, since no typed call could ever satisfy such a condition.
tests/types/common/config-authoring.test.tsholds all of this with@ts-expect-error, the first negative type tests in the package. The 110 existing assertions only ever proved what compiles, which is exactly how three gaps survived.One configuration shape stops compiling. Requiring
extendcloses the overload that used to accept anything, and that overload was also what accepted a configuration whose literal types had widened — a hoistedconst defaultVariants = { size: "sm" }, a hoistedcompoundVariantsarray, a spread of a shared partial. Those have type{ size: string }, which was never assignable to{ size?: "sm" }; they compiled only because the catch-all widened the schema to swallow them. Addas constto the hoisted value, or inline it.The error TypeScript reports for this is
Property 'extend' is missing, which names the last overload tried rather than the real mismatch. It is the same message a plain variant typo now produces. Nothing in this repository hit either case, but a consumer with a shared configuration fragment will.#700
93b18acThanks @thevuong! - A variant function now remembers what each selection resolved to, so a repeated selection skips both the plan walk andtailwind-merge. Against the previous build the resolution rows measure 1.08× to 11.9×, and againsttailwind-variantsthe suite geomean moves from 6.18× to 19.8×. The motivating measurement: in the merged lane most of the cost was never the merge algorithm —tailwind-mergecaches — but building and hashing the joined class string to look that cache up, which a key built from the selection avoids entirely.The key is a mixed-radix number, one digit per variant. A variant no compound tests is keyed by its group key, since two values sharing a key select the same classes; a variant a compound tests is keyed by the raw value, because a compound compares against what the caller passed and
trueand"true"share a group key while comparing differently. A call the key cannot represent — an axis past its capacity, a configuration too large to address in one safe integer, a clsx-shapedclassName— resolves the long way as before.Two consequences are worth knowing. A slot component called twice with the same selection gets back the same object of slot functions: stable enough for a React dependency array, and shared, so nothing may mutate it. And the store is bounded and keyed by the selection, so a component whose variant values are effectively unique per call fills it with entries nothing reads again — the new
cacheResolutions: falseoption turns it off for that component.Alongside it:
extendTailwindMergeis now memoised bytwMergeConfigidentity, so a design system handing one config to a hundred components builds one merge function instead of a hundred, each with its own cache; a slot resolution keeps only the props a compound can read rather than the caller's whole props object, which would otherwise pinchildrenfor as long as the entry lives; and each slot's merged text is memoised, so re-reading a slot no longer re-runs the merge.tvitself costs about a quarter of a microsecond more per component definition. The encoder is compiled on first resolution rather than intv, so a component that is defined and never rendered pays nothing for it.#701
710d533Thanks @thevuong! -@codefast/tailwind-variantsnow ships with no runtime dependencies of its own, matching whattailwind-variantshas always done. Two changes get it there.clsxis gone, replaced by a flattener inside the package. It is deliberately identical, corners included: abigintcontributes nothing despite being aClassValue, and an object's keys are read withfor…in, so an inherited enumerable one counts. Verified by running the behaviour sweep — every configuration, every variant value, every slot, with and without merging — againstmainand diffing: zero difference across 118,505 outcomes. The dependency was only ever reached at compile time anyway, since resolution stopped calling it when the plan was introduced.ClassValueis now declared here rather than re-exported from clsx, so the type survives the dependency leaving. Its shape is unchanged except thatClassDictionaryisRecord<string, unknown>rather than clsx'sRecord<string, any>— which means passing a function where a class value is expected is now a type error. It contributed nothing at runtime either way.tailwind-mergemoves from a dependency to a peer (>=3.0.0). A consumer who pins their own version now gets one copy at the version they chose, instead of a second one arriving underneath this package. Install it alongside —@codefast/uialready does on your behalf.#700
d0dd326Thanks @thevuong! - Compile the configuration once, intv, instead of re-deriving it on every resolver call.tvnow builds a plan. Variant groups become an array of entries whose default classes are already looked up, compound variants become flat condition lists whose fallbacks are already resolved, and every class value is flattened to a string. Resolution reads monomorphic fields and concatenates strings, so the per-call work that used to happen —Object.keysper compound variant, a dictionary lookup per variant per slot, an intermediate array plus a spread intoclsx— is gone.Slot resolution is also inverted. A slot map names only a few slots, so having each of N slots scan every variant meant mostly-missing lookups. Each variant value now carries the slot positions it targets, and one pass distributes classes into a per-slot buffer that every resolver of that call shares; a slot called without its own props just reads its entry. Compound slots ride the same pass.
Measured against the previous release on
benchmarks/tailwind-variants(paired A/B, one subprocess per side per scenario, three passes with the order alternated, median of per-pass ratios): every one of the sixteen scenarios is faster, geometric mean 2.86×.shape without tailwind-mergewith extreme-slots9.64× 5.99× slots5.55× 3.48× compound-slots4.94× 3.21× complex2.59× 2.00× simple/extends/create-tv/extreme2.22×–2.33× 1.56×–1.89× The merge-enabled rows gain least because merging, not resolution, is what is left in them. The suite's A/A noise floor on the same machine is ±1%.
The trade is a slower
tvcall: flattening every class value and precomputing slot positions costs roughly 400ns more for a simple config and ~3.4µs for a ten-slot one. That is once per component definition, at module load, and against a slot resolution that got ~1.6µs cheaper it pays for itself after about two renders.One behavioural change, in a corner: a configuration whose classes are all truthy but render to nothing —
tv({ base: {} })— now returnsundefinedwithtwMergeenabled, where it previously returned"". It already returnedundefinedwithtwMergedisabled, soundefinedis now what "no classes" means either way.
Patch Changes
#700
fe7e9e4Thanks @thevuong! - A variant value naming anObject.prototypemember no longer reaches it. A compiled variant group is indexed by whatever a caller passes, and it was a plain object — sogroup["toString"]answered with a function instead ofundefined. The flat lane concatenated that function's source text into the class string, and the slot lane read slot positions offObject.prototypeand threw:tv({ base: "block", variants: { size: { sm: "p-2" } } })({ size: "toString" }) → "block function toString() { [native code] }" tv({ slots: { base: "rounded" }, … })({ size: "__proto__" }).base() → TypeError: Cannot read properties of undefined (reading 'length')Groups and the slot index map are now compiled onto prototype-less objects, which closes both resolvers and the selection encoder at once since all three read the same object. It costs roughly twice what reusing the source object did, once per component definition.
Two smaller things fall out of the same reading. A value the group does not answer is no longer memoised into the id table, which a long-lived server rendering user-supplied values would otherwise grow without bound; and inherited keys no longer consume the ids a group's declared values need, which used to disable a resolver's cache permanently after a few junk values.
0.5.0
Patch Changes
#676
641e233Thanks @thevuong! - Collapse thetypesanddefaultlanes ofpackage.json#importsfrom fallback arrays to single strings.Node resolves an imports array by taking the first candidate it can parse, without checking that the file exists and without falling through — a specifier whose first candidate is missing throws
ERR_MODULE_NOT_FOUNDrather than trying the second../dist/*/index.jsand./dist/*/index.d.tscould therefore never be reached, so they read as a safety net that does not exist. Thesourcelane keeps its extension candidates, which onlytscand Vite read and both probe.
0.5.0-canary.9
Patch Changes
#676
641e233Thanks @thevuong! - Collapse thetypesanddefaultlanes ofpackage.json#importsfrom fallback arrays to single strings.Node resolves an imports array by taking the first candidate it can parse, without checking that the file exists and without falling through — a specifier whose first candidate is missing throws
ERR_MODULE_NOT_FOUNDrather than trying the second../dist/*/index.jsand./dist/*/index.d.tscould therefore never be reached, so they read as a safety net that does not exist. Thesourcelane keeps its extension candidates, which onlytscand Vite read and both probe.
0.5.0-canary.8
0.5.0-canary.7
0.5.0-canary.6
1.0.0-canary.7
1.0.0-canary.6
0.5.0-canary.5
0.5.0-canary.4
0.5.0-canary.3
0.5.0-canary.2
Showing the latest 20 releases; the full changelog on GitHub has 45 more releases.