npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

prettier-plugin-cn

v1.0.0

Published

Prettier plugin to auto-wrap className with cn()

Downloads

179

Readme

prettier-plugin-cn

Prettier plugin that automatically wraps React className attributes with the cn() utility function, perfect for shadcn/ui and Tailwind CSS projects.

Features

  • Zero Configuration: Auto-detects cn function location in 95% of projects
  • Multiple Pattern Support: Transforms 5 different className patterns
  • Idempotent: Running multiple times produces identical output
  • Fast: <5% overhead on formatting time
  • Smart Import Management: Automatically adds and manages imports
  • Intelligent Function Handling: Configure alternate functions (clsx, twMerge) to ignore or replace, automatically wrap others

Installation

npm install --save-dev prettier-plugin-cn
# or
pnpm add -D prettier-plugin-cn
# or
yarn add -D prettier-plugin-cn

Quick Start

  1. Add the plugin to your Prettier configuration:
{
  "plugins": ["prettier-plugin-cn"]
}
  1. Format your files:
npx prettier --write "**/*.{tsx,jsx}"

That's it! The plugin will automatically detect your cn function and wrap all className attributes.

What It Does

Pattern A: String Literals

// Before
<div className="flex flex-col gap-4" />

// After
<div className={cn("flex flex-col gap-4")} />

Pattern B: Template Literals

// Before
<div className={`flex ${isActive && "active"}`} />

// After
<div className={cn("flex", isActive && "active")} />

Pattern C: Conditionals

// Before
<div className={isActive ? "active" : "inactive"} />

// After
<div className={cn(isActive ? "active" : "inactive")} />

Pattern D: Function Call Handling

The plugin intelligently handles function calls in className based on whether they're "alternate" cn() implementations.

Default behavior (mode='ignore'): Alternate functions are left untouched

// Before
<div className={clsx("flex", isActive && "active")} />

// After (default: mode='ignore')
<div className={clsx("flex", isActive && "active")} />  // No change

With mode='replace': Replace alternate function names with cn()

// Before
<div className={clsx("flex", isActive && "active")} />

// After (mode='replace')
<div className={cn("flex", isActive && "active")} />

Non-alternate functions: Always wrapped with cn()

// Before
<div className={customFn("flex", isActive && "active")} />

// After (any mode)
<div className={cn(customFn("flex", isActive && "active"))} />

Pattern E: Multiline Strings

// Before
<div className=`
  flex flex-col
  bg-white dark:bg-gray-800
  rounded-lg
` />

// After
<div className={cn(
  "flex flex-col",
  "bg-white dark:bg-gray-800",
  "rounded-lg"
)} />

Configuration

Default Configuration

The plugin works out of the box with sensible defaults:

{
  "cnUtilFnName": "cn",
  "cnUtilFnPath": undefined, #auto detects off default paths, set to overwrite
  "cnUtilFnImportType": "named",
  "cnAutoImport": true,
  "cnAlternateFunctions": ["clsx", "classnames", "cx", "classNames", "twMerge"],
  "cnAlternateFunctionsMode": "ignore"
}

Note: Import path and type are auto-detected from your project when not explicitly set.

Configuration Options

cnUtilFnName

Type: string Default: "cn"

The name of the utility function to wrap classNames with.

{
  "cnUtilFnName": "classNames"
}

cnUtilFnPath

Type: string | undefined Default: undefined (auto-detected)

Explicit import path for the utility function. Leave undefined to auto-detect from:

  • components.json (shadcn/ui config)
  • tsconfig.json / jsconfig.json path aliases
  • Common file locations (src/lib/utils.ts, etc.)
{
  "cnUtilFnPath": "@/utils/classnames"
}

cnUtilFnImportType

Type: "named" | "default" Default: Auto-detected from file

Automatically detected by reading the export style from the resolved file. Only set explicitly if detection fails.

See [Import Path Detection](#Import Path Detection)

{
  "cnUtilFnImportType": "default"
}

Result:

// named (auto-detected from: export function cn)
import { cn } from '@/lib/utils'

// default (auto-detected from: export default cn)
import cn from '@/lib/utils'

cnAutoImport

Type: boolean Default: true

Automatically add import statement if missing.

{
  "cnAutoImport": false
}

cnAlternateFunctions

Type: string[] Default: ['clsx', 'classnames', 'cx', 'classNames', 'twMerge']

List of function names to treat as alternate cn() implementations. These functions receive special handling based on cnAlternateFunctionsMode.

All other function calls (not in this list) will be wrapped with cn().

{
  "cnAlternateFunctions": ["clsx", "classnames", "twMerge"]
}

cnAlternateFunctionsMode

Type: 'ignore' | 'replace' Default: 'ignore'

Controls how alternate functions (from cnAlternateFunctions list) are handled.

Mode: 'ignore' (default) - Skip transformation entirely

// Before
className={clsx('flex', isActive && 'active')}

// After (no change)
className={clsx('flex', isActive && 'active')}

Alternate functions are left untouched. Use this when you want to keep using libraries like clsx or twMerge in your codebase.

Mode: 'replace' - Replace function name with cn()

// Before
className={clsx('flex', isActive && 'active')}

// After
className={cn('flex', isActive && 'active')}

Use this mode to migrate away from alternate functions and standardize on cn().

Configuration:

{
  "cnAlternateFunctions": ["clsx", "classnames", "twMerge"],
  "cnAlternateFunctionsMode": "replace"
}

Important: Functions NOT in the alternate list are always wrapped:

// customFn is not in the alternate list
// Before
className={customFn('flex')}

// After (regardless of mode)
className={cn(customFn('flex'))}

Auto-Detection

The plugin automatically detects your cn function when cnUtilFnPath is not set:

Import Path Detection

Checks in priority order:

  1. components.json (shadcn/ui): aliases.utils
  2. tsconfig.json/jsconfig.json: Path aliases resolving to:
    • src/lib/utils.ts
    • src/utils/cn.ts
  3. File system: Common locations:
    • src/lib/utils.{ts,js}
    • lib/utils.{ts,js}
    • src/utils/cn.{ts,js}
    • utils/cn.{ts,js}

Import Type Detection

Automatically detects export style from resolved file:

// Detected as 'named'
export function cn(...inputs) { ... }
export const cn = (...inputs) => { ... }
export { cn }

// Detected as 'default'
export default function cn(...inputs) { ... }
export default cn

Debugging

Enable debug logging:

# Using environment variable
PRETTIER_DEBUG=1 npx prettier --write "**/*.tsx"

# Or set log level
PRETTIER_PLUGIN_CN_LOG_LEVEL=DEBUG npx prettier --write "**/*.tsx"

Log levels: ERROR, WARN, INFO, DEBUG

Idempotency

Running the plugin multiple times produces the same output:

# First run
npx prettier --write Button.tsx

# Second run (no changes)
npx prettier --write Button.tsx

Already wrapped classNames are not modified:

// This stays unchanged
<div className={cn('flex', isActive && 'active')} />

Integration

With ESLint

{
  "extends": ["plugin:prettier/recommended"],
  "plugins": ["prettier"]
}

With VS Code

Add to .vscode/settings.json:

{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode"
}

With Husky/lint-staged

{
  "lint-staged": {
    "*.{tsx,jsx}": ["prettier --write"]
  }
}

Troubleshooting

Import not added

Issue: Plugin transforms className but doesn't add import

Solutions:

  1. Check autoImport is enabled (default: true)
  2. Verify cn function is exported in detected file
  3. Enable debug logging to see detection results

Wrong import path

Issue: Plugin uses wrong import path

Solutions:

  1. Set explicit importPath in configuration
  2. Update components.json or tsconfig.json paths
  3. Check file exists at auto-detect paths

No transformation happening

Issue: className not being wrapped

Solutions:

  1. Verify plugin is loaded: npx prettier --list-plugins
  2. Check file extension is .tsx or .jsx
  3. Enable debug logging to see what's happening

Performance

Benchmarks on average project:

  • Single file (<1000 lines): <50ms overhead
  • Large file (>1000 lines): <100ms overhead
  • Full project (100 files): <5s total overhead

Examples

See examples/ directory for complete working examples:

  • Basic React app
  • Next.js app
  • shadcn/ui project
  • Custom configuration

Contributing

Contributions welcome! See CONTRIBUTING.md for guidelines.

License

MIT

Credits

Built for use with shadcn/ui and Tailwind CSS.