Skip to content

Make Tailwind optional#16

Merged
josephdburdick merged 7 commits into
mainfrom
feat/tailwind-optional
Apr 25, 2026
Merged

Make Tailwind optional#16
josephdburdick merged 7 commits into
mainfrom
feat/tailwind-optional

Conversation

@josephdburdick
Copy link
Copy Markdown
Contributor

@josephdburdick josephdburdick commented Apr 25, 2026

Summary by CodeRabbit

  • Chores

    • Version bumped to 1.2.8
    • tailwind-merge moved to an optional peer dependency while retained for development
    • TypeScript config updated to suppress certain deprecation diagnostics and include Node typings
    • Tooling schema reference bumped
  • Behavior

    • Class-name merging is now loaded optionally at runtime and will gracefully fall back if unavailable
  • Tests / Dev tooling

    • Test environment switched to a different DOM runtime; test/build tooling versions adjusted and an additional test runtime added

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 25, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e0194657-4d1a-485e-aed3-81176390210d

📥 Commits

Reviewing files that changed from the base of the PR and between a09ba6c and 0506aaa.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (1)
  • package.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • package.json

📝 Walkthrough

Walkthrough

Bumps package to 1.2.8, makes tailwind-merge an optional peer (kept in dev), loads it dynamically at runtime, switches tests to happy-dom, pins/downgrades Vite/Vitest tooling, and adds TypeScript deprecation ignore and Node typings.

Changes

Cohort / File(s) Summary
Package manifest
package.json
Version -> 1.2.8. Moves tailwind-merge from dependencies to peerDependencies and marks it optional via peerDependenciesMeta; retains tailwind-merge in devDependencies. Adds happy-dom and adjusts tooling versions for vite, vitest, @vitejs/plugin-react, and @vitest/coverage-istanbul.
Runtime utility
src/utils.ts
Replaces static tailwind-merge import with export async function initTailwindMerge() that dynamically imports tailwind-merge if available. Module-level twMerge is set on success; cn(...inputs) now requires initialization (throws if not awaited) and applies twMerge only when present, otherwise returns clsx output.
TypeScript configs
tsconfig.json, playground/tsconfig.json
Adds ignoreDeprecations: "6.0" to suppress deprecation diagnostics. Root tsconfig.json also adds types: ["node"]. playground/tsconfig.json receives the ignoreDeprecations option.
Test config
vitest.config.ts
Changes test environment from jsdom to happy-dom while keeping other test settings unchanged.
Tooling metadata
biome.json
Bumps $schema reference from Biome 2.4.12 to 2.4.13 (schema metadata update).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hop and fetch a module on the fly,
optional tails waving as I try,
tests don happy hats, TypeScript hums low,
version hopped forward — off I go!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Make Tailwind optional' directly reflects the main objective of the PR: converting tailwind-merge from a required dependency to an optional peer dependency with fallback behavior in the cn() function.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/tailwind-optional

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/utils.ts (1)

15-16: Add regression tests for both cn execution paths.

This change introduces two runtime modes (twMerge present vs absent), but current tests only cover basic concatenation. Add explicit tests for both modes so this optional-peer behavior stays stable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils.ts` around lines 15 - 16, Tests are missing coverage for both
execution paths of the cn utility (with and without the optional twMerge
behavior); add two unit tests: one that constructs inputs for cn when twMerge is
undefined/absent and asserts it returns the expected concatenated class string
(verifying clsx logic), and another that supplies a mock twMerge implementation
to cn and asserts that the returned value is the result of twMerge(classes)
(verifying the branch where twMerge is used); reference the cn function, the
twMerge parameter, and clsx to locate the implementation and ensure both
branches (twMerge ? twMerge(classes) : classes) are exercised.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@package.json`:
- Around line 50-52: Replace the nonstandard peerDependenciesOptional entry by
adding "tailwind-merge" to peerDependencies and marking it optional via
peerDependenciesMeta: move the dependency from peerDependenciesOptional into the
peerDependencies object and add a peerDependenciesMeta object with
"tailwind-merge": { "optional": true }; remove the peerDependenciesOptional key
entirely and ensure package.json remains valid JSON.

In `@src/utils.ts`:
- Around line 5-8: Replace the static CommonJS require usage with a dynamic
import to make tailwind-merge an optional runtime dependency: in the block where
you currently call require('tailwind-merge') and assign twMerge, change the
logic to use import('tailwind-merge').catch(() => undefined) and then, if the
module exists, set twMerge = module.twMerge (or module?.default?.twMerge) so
bundlers like Vite/Rollup/Webpack won't try to resolve the dependency at build
time; update any code that relies on twMerge to handle it being undefined.

---

Nitpick comments:
In `@src/utils.ts`:
- Around line 15-16: Tests are missing coverage for both execution paths of the
cn utility (with and without the optional twMerge behavior); add two unit tests:
one that constructs inputs for cn when twMerge is undefined/absent and asserts
it returns the expected concatenated class string (verifying clsx logic), and
another that supplies a mock twMerge implementation to cn and asserts that the
returned value is the result of twMerge(classes) (verifying the branch where
twMerge is used); reference the cn function, the twMerge parameter, and clsx to
locate the implementation and ensure both branches (twMerge ? twMerge(classes) :
classes) are exercised.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7d2f0a87-3f91-4fd9-a843-7bffe344c952

📥 Commits

Reviewing files that changed from the base of the PR and between 69fd833 and 5ba1881.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (5)
  • package.json
  • playground/tsconfig.json
  • src/utils.ts
  • tsconfig.json
  • vitest.config.ts

Comment thread package.json Outdated
Comment thread src/utils.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@package.json`:
- Around line 68-81: The dependency versions in package.json are incompatible:
`@vitejs/plugin-react` (^6.0.1) requires Vite 8+, and `@vitest/coverage-istanbul`
(^4.1.5) expects Vitest 4.x while your vitest is ^2.1.9; fix by aligning
versions—either downgrade `@vitejs/plugin-react` to a 5.x release that supports
vite@^5.4.21 or upgrade vite to a compatible 8.x/9.x range to match
`@vitejs/plugin-react`@^6.0.1, and likewise either downgrade
`@vitest/coverage-istanbul` to a 2.x-compatible release or upgrade vitest to
^4.1.5 to match `@vitest/coverage-istanbul`@^4.1.5; update the package.json
entries (the "vite", "@vitejs/plugin-react", "vitest", and
"@vitest/coverage-istanbul" fields), then reinstall (npm/yarn/pnpm) and run the
test/build to confirm compatibility.

In `@src/utils.ts`:
- Around line 6-17: Remove the top-level side-effect import and instead add an
exported async initializer (e.g., initTailwindMerge) that performs the dynamic
import('tailwind-merge') and sets an internal twMerge and a boolean flag (e.g.,
isTwMergeInitialized); modify cn(...) to check that flag and either throw a
clear error if initTailwindMerge wasn't awaited (forcing callers to initialize
before first render) or use a documented deterministic fallback merge function,
and ensure you reference the existing symbols twMerge, cn, and clsx when making
the changes so callers can find and await initTailwindMerge before using cn.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c247b052-3ee5-4a4e-bb2c-82660f5de902

📥 Commits

Reviewing files that changed from the base of the PR and between 5ba1881 and baf38e6.

📒 Files selected for processing (3)
  • biome.json
  • package.json
  • src/utils.ts
✅ Files skipped from review due to trivial changes (1)
  • biome.json

Comment thread package.json Outdated
Comment thread src/utils.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/utils.ts`:
- Around line 23-27: The cn() function currently throws if isTwMergeInitialized
is false, breaking existing synchronous call paths; change this to preserve a
synchronous, deterministic API by removing the throw in cn() and instead
performing a safe fallback or on-demand synchronous initialization: either
synchronously call a non-async initializer (or a synchronous wrapper around
initTailwindMerge()) or default to a deterministic merge behavior when
isTwMergeInitialized is false. Locate the check using isTwMergeInitialized
inside cn() and replace the throw with logic that ensures cn() can run
synchronously (e.g., call a sync initTailwindMerge() helper or use a safe
default merge) so callers need not await initTailwindMerge().
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e8d012a4-5cbe-436c-aa13-55777f12e9cf

📥 Commits

Reviewing files that changed from the base of the PR and between baf38e6 and a09ba6c.

📒 Files selected for processing (1)
  • src/utils.ts

Comment thread src/utils.ts
Comment on lines +23 to +27
if (!isTwMergeInitialized) {
throw new Error(
'initTailwindMerge() must be awaited before using cn(). Call await initTailwindMerge() during initialization.',
)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

cn() now throws on existing synchronous call paths (runtime-breaking change).

At Line 23, cn() throws unless initTailwindMerge() was awaited first. Current usage patterns call cn() directly (e.g., src/utils.test.ts:1-9, and component consumers via src/components/Drawer.tsx / src/index.ts exports), so this introduces immediate runtime failures and a breaking API shift.

Proposed fix (keep API synchronous + deterministic)
 let twMerge: ((input: string) => string) | undefined
 let isTwMergeInitialized = false
+let didUseCn = false

 export async function initTailwindMerge(): Promise<void> {
-  if (isTwMergeInitialized) {
+  if (isTwMergeInitialized || didUseCn) {
     return
   }

   try {
     const module = await import('tailwind-merge')
-    twMerge = module?.twMerge || module?.default?.twMerge
+    if (!didUseCn) {
+      twMerge = module?.twMerge || module?.default?.twMerge
+    }
   } catch {
     // tailwind-merge is not available, use fallback
-    twMerge = undefined
+    if (!didUseCn) {
+      twMerge = undefined
+    }
   }

   isTwMergeInitialized = true
 }

 export function cn(...inputs: ClassValue[]) {
-  if (!isTwMergeInitialized) {
-    throw new Error(
-      'initTailwindMerge() must be awaited before using cn(). Call await initTailwindMerge() during initialization.',
-    )
-  }
+  didUseCn = true

   const classes = clsx(inputs)
   return twMerge ? twMerge(classes) : classes
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!isTwMergeInitialized) {
throw new Error(
'initTailwindMerge() must be awaited before using cn(). Call await initTailwindMerge() during initialization.',
)
}
let twMerge: ((input: string) => string) | undefined
let isTwMergeInitialized = false
let didUseCn = false
export async function initTailwindMerge(): Promise<void> {
if (isTwMergeInitialized || didUseCn) {
return
}
try {
const module = await import('tailwind-merge')
if (!didUseCn) {
twMerge = module?.twMerge || module?.default?.twMerge
}
} catch {
// tailwind-merge is not available, use fallback
if (!didUseCn) {
twMerge = undefined
}
}
isTwMergeInitialized = true
}
export function cn(...inputs: ClassValue[]) {
didUseCn = true
const classes = clsx(inputs)
return twMerge ? twMerge(classes) : classes
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils.ts` around lines 23 - 27, The cn() function currently throws if
isTwMergeInitialized is false, breaking existing synchronous call paths; change
this to preserve a synchronous, deterministic API by removing the throw in cn()
and instead performing a safe fallback or on-demand synchronous initialization:
either synchronously call a non-async initializer (or a synchronous wrapper
around initTailwindMerge()) or default to a deterministic merge behavior when
isTwMergeInitialized is false. Locate the check using isTwMergeInitialized
inside cn() and replace the throw with logic that ensures cn() can run
synchronously (e.g., call a sync initTailwindMerge() helper or use a safe
default merge) so callers need not await initTailwindMerge().

@josephdburdick josephdburdick merged commit a212287 into main Apr 25, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant