feat: add configure() for custom Tailwind theme utilities - #14
Open
zwarunek wants to merge 1 commit into
Open
Conversation
cn bakes in the default tailwind-merge configuration and exposes no way to
extend it, so a utility from a customised theme is classified wrong and
silently dropped: cn("text-xxs text-muted-foreground") returns just
"text-muted-foreground", because text-xxs is not in the default text scale and
falls through to the text-color group.
Adds mergeConfigs (ported from tailwind-merge, MIT) and a configure() entry
point that registers an { override, extend } extension for the shared cn and
twMerge. The extension is read once, by the existing lazy initialiser on the
first merge, so an unconfigured cn is unchanged and no per-call work is added.
Configuring after the first merge throws rather than dropping the caches: the
same input would merge to one string early in the process and another later,
and the strings already returned cannot be recalled.
aidenybai
force-pushed
the
feat/configure
branch
from
July 27, 2026 22:20
f469eca to
df34a2b
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
cnbakes in the default tailwind-merge configuration and exposes no way to extend it, so any utility that comes from a customised Tailwind theme is classified wrong and silently dropped.What breaks today
text-xxsis not in the defaulttexttheme scale, so it falls through to the text-color group and conflicts withtext-muted-foreground.tailwind-mergewithextendTailwindMerge({ extend: { theme: { text: ["xxs"] } } })keeps both. Every custom type scale hits this; on a design system with ten type tokens, 22 of our 35 merge assertions fail.There is no workaround on 0.0.8 short of patching the published bundle:
createTailwindMerge,getDefaultConfigandmergeConfigsare not exported, andexportsis.-only so they cannot be deep-imported either. We currently run cnfast in production behind a pnpm patch that does what this PR does, which is why I would rather send it here.What this changes
src/lib/merge-configs.tsaddsmergeConfigs, ported from tailwind-merge (MIT):overridereplaces a config property,extendappends to it.configure(extension)insrc/lib/tw-merge.tsstores an extension, exported from the entry point along with theConfigExtensiontype. The merge config is built from it lazily on the first merge, by thecreateTailwindMergeinitialiser that already exists.configuretargets the sharedcndeliberately.cnfast migraterewrites imports tocnfast, andregistry/cnfast/utils.tsisexport { cn } from "cnfast"— both onboarding paths land users on the default instance, so a configuration hook that cannot reach it does not help them.Calling
configureafter the first merge throws rather than dropping the caches. Re-configuring a live instance means the same input merges to one string early in the process and a different one later, and strings already returned to a caller cannot be recalled; in React that surfaces as a hydration mismatch. Making it one-shot states the constraint instead of hiding it, and means there is nothing to invalidate: at configure time the whole-string, descriptor, arg-sequence and template caches are all still empty.One open question. Should a second
configurebefore the first merge also throw? Right now the last call wins. Throwing would catch two libraries fighting over the shared instance, at the cost of breaking HMR when an entry module re-evaluates. I left it permissive, and will flip it if you prefer.What this does not change
An unconfigured
cnis byte-identical:configExtensionstaysundefinedandcreateConfigreturnsgetDefaultConfig()exactly as before. Nothing lands on a hot path — the extension is read once inside the lazy initialiser, with no per-call branch, allocation or property read added. No existing export changes.Relationship to #7
#7 is open and unreviewed since 2026-06-23 and fixes the same defect from the other direction: a
createCn(config)factory returning an isolated configured instance. I checked it out against currentmain— it merges cleanly, its 149 tests pass, and it solves our case too. It contains the samemergeConfigsport as this PR, from the same source.The two are complementary rather than competing:
configurefor the app-wide default instance thatmigrateand the registry point at,createCnfor library authors, tests, and anything needing more than one configuration in a process. If you take #7 first I will rebase this on top soconfigurebecomes a thin one-shot binding over its factory. If you want only one of the two shapes, say which and I will close the other out of the way. Either way #6, #8 and #13 are all waiting on this.Checks
pnpm build,pnpm typecheck, andpnpm lintpass; lint reports 14 warnings before and after this branch, none in the new files.pnpm testgoes from 130 to 148, 18 new. With the source change reverted those 18 fail: 9 assertion failures inconfigure.test.ts, andmerge-configs.test.tscannot resolve the module.pnpm format:checkalready fails onmain, on four markdown files this branch does not touch (README.md,docs/upstream-tailwind-merge.md,packages/cnfast/README.md,packages/cnfast/bench/README.md). I ranvp fmt, kept its output for the files in this PR and reverted the rest so the diff stays reviewable — the other four look like a separate housekeeping commit.Note
Medium Risk
Changes global merge semantics for every consumer of the default
cn/twMergewhen configured, with ordering constraints that can break apps ifconfigureruns too late; unconfigured paths are unchanged.Overview
Adds
configure({ override, extend })so the sharedcnandtwMergecan register tailwind-merge-style extensions (e.g. customtextscale tokens likexxs) before the first merge. The merge config is built lazily on first use viamergeConfigs; callers who never configure keep the same default behavior.Late
configurethrows after any merge has run, so already-returned class strings are not silently inconsistent. README documents usage and the one-shot ordering requirement.New Vitest coverage exercises theme/class-group extend and override, all
cncall shapes,twMerge, default behavior withoutconfigure, and misuse;merge-configsbehavior is covered in a dedicated test file. Minor release noted in changeset.Reviewed by Cursor Bugbot for commit df34a2b. Bugbot is set up for automated code reviews on this repo. Configure here.