Socket
Socket
Sign inDemoInstall

tailwind-merge

Package Overview
Dependencies
Maintainers
1
Versions
276
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

tailwind-merge - npm Package Compare versions

Comparing version 2.3.0 to 2.4.0-dev.1d82a24b93b50392a3a3ed22281d5b18130b5fab

src/lib/class-group-utils.ts

34

package.json
{
"name": "tailwind-merge",
"version": "2.3.0",
"version": "2.4.0-dev.1d82a24b93b50392a3a3ed22281d5b18130b5fab",
"description": "Merge Tailwind CSS classes without style conflicts",

@@ -58,3 +58,2 @@ "keywords": [

"lint": "eslint --max-warnings 0 '**'",
"size": "size-limit",
"preversion": "if [ -n \"$DANYS_MACHINE\" ]; then git checkout main && git pull; fi",

@@ -64,30 +63,27 @@ "version": "zx scripts/update-readme.mjs",

},
"dependencies": {
"@babel/runtime": "^7.24.1"
},
"dependencies": {},
"devDependencies": {
"@babel/plugin-transform-runtime": "^7.24.3",
"@babel/preset-env": "^7.24.3",
"@babel/core": "^7.24.7",
"@babel/preset-env": "^7.24.7",
"@rollup/plugin-babel": "^6.0.4",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-typescript": "^11.1.6",
"@size-limit/preset-small-lib": "^11.1.2",
"@types/jest": "^29.5.12",
"@typescript-eslint/eslint-plugin": "^7.5.0",
"@typescript-eslint/parser": "^7.5.0",
"@typescript-eslint/eslint-plugin": "^7.14.1",
"@typescript-eslint/parser": "^7.14.1",
"babel-plugin-annotate-pure-calls": "^0.4.0",
"babel-plugin-polyfill-regenerator": "^0.6.1",
"babel-plugin-polyfill-regenerator": "^0.6.2",
"eslint": "^8.57.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jest": "^27.9.0",
"eslint-plugin-jest": "^28.6.0",
"globby": "^11.1.0",
"jest": "^29.7.0",
"prettier": "^3.2.5",
"rollup": "^4.13.2",
"prettier": "^3.3.2",
"rollup": "^4.18.0",
"rollup-plugin-delete": "^2.0.0",
"rollup-plugin-dts": "^6.1.0",
"size-limit": "^11.1.2",
"ts-jest": "^29.1.2",
"typescript": "^5.4.3",
"zx": "^7.2.3"
"rollup-plugin-dts": "^6.1.1",
"ts-jest": "^29.1.5",
"tslib": "^2.6.3",
"typescript": "^5.5.2",
"zx": "^8.1.3"
},

@@ -94,0 +90,0 @@ "publishConfig": {

@@ -6,3 +6,3 @@ <!-- This file is autogenerated. If you want to change this content, please do the changes in `./docs/README.md` instead. -->

<a href="https://github.com/dcastil/tailwind-merge">
<img src="https://github.com/dcastil/tailwind-merge/raw/v2.3.0/assets/logo.svg" alt="tailwind-merge" height="150px" />
<img src="https://github.com/dcastil/tailwind-merge/raw/1d82a24b93b50392a3a3ed22281d5b18130b5fab/assets/logo.svg" alt="tailwind-merge" height="150px" />
</a>

@@ -29,12 +29,12 @@ </div>

- [What is it for](https://github.com/dcastil/tailwind-merge/tree/v2.3.0/docs/what-is-it-for.md)
- [When and how to use it](https://github.com/dcastil/tailwind-merge/tree/v2.3.0/docs/when-and-how-to-use-it.md)
- [Features](https://github.com/dcastil/tailwind-merge/tree/v2.3.0/docs/features.md)
- [Limitations](https://github.com/dcastil/tailwind-merge/tree/v2.3.0/docs/limitations.md)
- [Configuration](https://github.com/dcastil/tailwind-merge/tree/v2.3.0/docs/configuration.md)
- [Recipes](https://github.com/dcastil/tailwind-merge/tree/v2.3.0/docs/recipes.md)
- [API reference](https://github.com/dcastil/tailwind-merge/tree/v2.3.0/docs/api-reference.md)
- [Writing plugins](https://github.com/dcastil/tailwind-merge/tree/v2.3.0/docs/writing-plugins.md)
- [Versioning](https://github.com/dcastil/tailwind-merge/tree/v2.3.0/docs/versioning.md)
- [Contributing](https://github.com/dcastil/tailwind-merge/tree/v2.3.0/docs/contributing.md)
- [Similar packages](https://github.com/dcastil/tailwind-merge/tree/v2.3.0/docs/similar-packages.md)
- [What is it for](https://github.com/dcastil/tailwind-merge/tree/1d82a24b93b50392a3a3ed22281d5b18130b5fab/docs/what-is-it-for.md)
- [When and how to use it](https://github.com/dcastil/tailwind-merge/tree/1d82a24b93b50392a3a3ed22281d5b18130b5fab/docs/when-and-how-to-use-it.md)
- [Features](https://github.com/dcastil/tailwind-merge/tree/1d82a24b93b50392a3a3ed22281d5b18130b5fab/docs/features.md)
- [Limitations](https://github.com/dcastil/tailwind-merge/tree/1d82a24b93b50392a3a3ed22281d5b18130b5fab/docs/limitations.md)
- [Configuration](https://github.com/dcastil/tailwind-merge/tree/1d82a24b93b50392a3a3ed22281d5b18130b5fab/docs/configuration.md)
- [Recipes](https://github.com/dcastil/tailwind-merge/tree/1d82a24b93b50392a3a3ed22281d5b18130b5fab/docs/recipes.md)
- [API reference](https://github.com/dcastil/tailwind-merge/tree/1d82a24b93b50392a3a3ed22281d5b18130b5fab/docs/api-reference.md)
- [Writing plugins](https://github.com/dcastil/tailwind-merge/tree/1d82a24b93b50392a3a3ed22281d5b18130b5fab/docs/writing-plugins.md)
- [Versioning](https://github.com/dcastil/tailwind-merge/tree/1d82a24b93b50392a3a3ed22281d5b18130b5fab/docs/versioning.md)
- [Contributing](https://github.com/dcastil/tailwind-merge/tree/1d82a24b93b50392a3a3ed22281d5b18130b5fab/docs/contributing.md)
- [Similar packages](https://github.com/dcastil/tailwind-merge/tree/1d82a24b93b50392a3a3ed22281d5b18130b5fab/docs/similar-packages.md)

@@ -13,3 +13,5 @@ export { createTailwindMerge } from './lib/create-tailwind-merge'

type DefaultThemeGroupIds,
type ExperimentalParseClassNameParam,
type ExperimentalParsedClassName,
} from './lib/types'
export * as validators from './lib/validators'

@@ -1,4 +0,4 @@

import { createClassUtils } from './class-utils'
import { createClassGroupUtils } from './class-group-utils'
import { createLruCache } from './lru-cache'
import { createSplitModifiers } from './modifier-utils'
import { createParseClassName } from './parse-class-name'
import { GenericConfig } from './types'

@@ -8,8 +8,6 @@

export function createConfigUtils(config: GenericConfig) {
return {
cache: createLruCache<string, string>(config.cacheSize),
splitModifiers: createSplitModifiers(config),
...createClassUtils(config),
}
}
export const createConfigUtils = (config: GenericConfig) => ({
cache: createLruCache<string, string>(config.cacheSize),
parseClassName: createParseClassName(config),
...createClassGroupUtils(config),
})

@@ -8,3 +8,3 @@ import { createTailwindMerge } from './create-tailwind-merge'

export function extendTailwindMerge<
export const extendTailwindMerge = <
AdditionalClassGroupIds extends string = never,

@@ -20,4 +20,4 @@ AdditionalThemeGroupIds extends string = never,

...createConfig: CreateConfigSubsequent[]
) {
return typeof configExtension === 'function'
) =>
typeof configExtension === 'function'
? createTailwindMerge(getDefaultConfig, configExtension, ...createConfig)

@@ -28,2 +28,1 @@ : createTailwindMerge(

)
}
import { DefaultThemeGroupIds, NoInfer, ThemeGetter, ThemeObject } from './types'
export function fromTheme<
export const fromTheme = <
AdditionalThemeGroupIds extends string = never,
DefaultThemeGroupIdsInner extends string = DefaultThemeGroupIds,
>(key: NoInfer<DefaultThemeGroupIdsInner | AdditionalThemeGroupIds>): ThemeGetter {
>(key: NoInfer<DefaultThemeGroupIdsInner | AdditionalThemeGroupIds>): ThemeGetter => {
const themeGetter = (theme: ThemeObject<DefaultThemeGroupIdsInner | AdditionalThemeGroupIds>) =>

@@ -8,0 +8,0 @@ theme[key] || []

@@ -9,3 +9,3 @@ // Export is needed because TypeScript complains about an error otherwise:

// LRU cache inspired from hashlru (https://github.com/dominictarr/hashlru/blob/v1.0.4/index.js) but object replaced with Map to improve performance
export function createLruCache<Key, Value>(maxCacheSize: number): LruCache<Key, Value> {
export const createLruCache = <Key, Value>(maxCacheSize: number): LruCache<Key, Value> => {
if (maxCacheSize < 1) {

@@ -22,3 +22,3 @@ return {

function update(key: Key, value: Value) {
const update = (key: Key, value: Value) => {
cache.set(key, value)

@@ -25,0 +25,0 @@ cacheSize++

import { ConfigUtils } from './config-utils'
import { IMPORTANT_MODIFIER, sortModifiers } from './modifier-utils'
import { IMPORTANT_MODIFIER, sortModifiers } from './parse-class-name'
const SPLIT_CLASSES_REGEX = /\s+/
export const mergeClassList = (classList: string, configUtils: ConfigUtils) => {
const { parseClassName, getClassGroupId, getConflictingClassGroupIds } = configUtils
export function mergeClassList(classList: string, configUtils: ConfigUtils) {
const { splitModifiers, getClassGroupId, getConflictingClassGroupIds } = configUtils
/**

@@ -16,85 +14,64 @@ * Set of classGroupIds in following format:

*/
const classGroupsInConflict = new Set<string>()
const classGroupsInConflict: string[] = []
return (
classList
.trim()
.split(SPLIT_CLASSES_REGEX)
.map((originalClassName) => {
const {
modifiers,
hasImportantModifier,
baseClassName,
maybePostfixModifierPosition,
} = splitModifiers(originalClassName)
let result = ''
let classGroupId = getClassGroupId(
maybePostfixModifierPosition
? baseClassName.substring(0, maybePostfixModifierPosition)
: baseClassName,
)
for (let i = classList.length - 1; i >= 0; ) {
while (classList[i] === ' ') {
--i
}
const nextI = classList.lastIndexOf(' ', i)
const originalClassName = classList.slice(nextI === -1 ? 0 : nextI + 1, i + 1)
i = nextI
let hasPostfixModifier = Boolean(maybePostfixModifierPosition)
const { modifiers, hasImportantModifier, baseClassName, maybePostfixModifierPosition } =
parseClassName(originalClassName)
if (!classGroupId) {
if (!maybePostfixModifierPosition) {
return {
isTailwindClass: false as const,
originalClassName,
}
}
let hasPostfixModifier = Boolean(maybePostfixModifierPosition)
let classGroupId = getClassGroupId(
hasPostfixModifier
? baseClassName.substring(0, maybePostfixModifierPosition)
: baseClassName,
)
classGroupId = getClassGroupId(baseClassName)
if (!classGroupId) {
if (!hasPostfixModifier) {
result = originalClassName + (result.length > 0 ? ' ' + result : result)
continue
}
if (!classGroupId) {
return {
isTailwindClass: false as const,
originalClassName,
}
}
classGroupId = getClassGroupId(baseClassName)
hasPostfixModifier = false
}
if (!classGroupId) {
result = originalClassName + (result.length > 0 ? ' ' + result : result)
continue
}
const variantModifier = sortModifiers(modifiers).join(':')
hasPostfixModifier = false
}
const modifierId = hasImportantModifier
? variantModifier + IMPORTANT_MODIFIER
: variantModifier
const variantModifier = sortModifiers(modifiers).join(':')
return {
isTailwindClass: true as const,
modifierId,
classGroupId,
originalClassName,
hasPostfixModifier,
}
})
.reverse()
// Last class in conflict wins, so we need to filter conflicting classes in reverse order.
.filter((parsed) => {
if (!parsed.isTailwindClass) {
return true
}
const modifierId = hasImportantModifier
? variantModifier + IMPORTANT_MODIFIER
: variantModifier
const { modifierId, classGroupId, hasPostfixModifier } = parsed
const classId = modifierId + classGroupId
const classId = modifierId + classGroupId
if (classGroupsInConflict.includes(classId)) {
continue
}
if (classGroupsInConflict.has(classId)) {
return false
}
classGroupsInConflict.push(classId)
classGroupsInConflict.add(classId)
const conflictGroups = getConflictingClassGroupIds(classGroupId, hasPostfixModifier)
for (let i = 0; i < conflictGroups.length; ++i) {
const group = conflictGroups[i]!
classGroupsInConflict.push(modifierId + group)
}
getConflictingClassGroupIds(classGroupId, hasPostfixModifier).forEach((group) =>
classGroupsInConflict.add(modifierId + group),
)
result = originalClassName + (result.length > 0 ? ' ' + result : result)
}
return true
})
.reverse()
.map((parsed) => parsed.originalClassName)
.join(' ')
)
return result
}

@@ -7,3 +7,3 @@ import { ConfigExtension, GenericConfig } from './types'

*/
export function mergeConfigs<ClassGroupIds extends string, ThemeGroupIds extends string = never>(
export const mergeConfigs = <ClassGroupIds extends string, ThemeGroupIds extends string = never>(
baseConfig: GenericConfig,

@@ -14,9 +14,11 @@ {

separator,
experimentalParseClassName,
extend = {},
override = {},
}: ConfigExtension<ClassGroupIds, ThemeGroupIds>,
) {
) => {
overrideProperty(baseConfig, 'cacheSize', cacheSize)
overrideProperty(baseConfig, 'prefix', prefix)
overrideProperty(baseConfig, 'separator', separator)
overrideProperty(baseConfig, 'experimentalParseClassName', experimentalParseClassName)

@@ -40,7 +42,7 @@ for (const configKey in override) {

function overrideProperty<T extends object, K extends keyof T>(
const overrideProperty = <T extends object, K extends keyof T>(
baseObject: T,
overrideKey: K,
overrideValue: T[K] | undefined,
) {
) => {
if (overrideValue !== undefined) {

@@ -51,6 +53,6 @@ baseObject[overrideKey] = overrideValue

function overrideConfigProperties(
const overrideConfigProperties = (
baseObject: Partial<Record<string, readonly unknown[]>>,
overrideObject: Partial<Record<string, readonly unknown[]>> | undefined,
) {
) => {
if (overrideObject) {

@@ -63,6 +65,6 @@ for (const key in overrideObject) {

function mergeConfigProperties(
const mergeConfigProperties = (
baseObject: Partial<Record<string, readonly unknown[]>>,
mergeObject: Partial<Record<string, readonly unknown[]>> | undefined,
) {
) => {
if (mergeObject) {

@@ -69,0 +71,0 @@ for (const key in mergeObject) {

@@ -32,3 +32,3 @@ /**

function toValue(mix: ClassNameArray | string) {
const toValue = (mix: ClassNameArray | string) => {
if (typeof mix === 'string') {

@@ -35,0 +35,0 @@ return mix

@@ -23,7 +23,57 @@ export interface Config<ClassGroupIds extends string, ThemeGroupIds extends string>

/**
* Theme scales used in classGroups.
* The keys are the same as in the Tailwind config but the values are sometimes defined more broadly.
* Allows to customize parsing of individual classes passed to `twMerge`.
* All classes passed to `twMerge` outside of cache hits are passed to this function before it is determined whether the class is a valid Tailwind CSS class.
*
* This is an experimental feature and may introduce breaking changes in any minor version update.
*/
experimentalParseClassName?(param: ExperimentalParseClassNameParam): ExperimentalParsedClassName
}
/**
* Type of param passed to the `experimentalParseClassName` function.
*
* This is an experimental feature and may introduce breaking changes in any minor version update.
*/
export interface ExperimentalParseClassNameParam {
className: string
parseClassName(className: string): ExperimentalParsedClassName
}
/**
* Type of the result returned by the `experimentalParseClassName` function.
*
* This is an experimental feature and may introduce breaking changes in any minor version update.
*/
export interface ExperimentalParsedClassName {
/**
* Modifiers of the class in the order they appear in the class.
*
* @example ['hover', 'dark'] // for `hover:dark:bg-gray-100`
*/
modifiers: string[]
/**
* Whether the class has an `!important` modifier.
*
* @example true // for `hover:dark:!bg-gray-100`
*/
hasImportantModifier: boolean
/**
* Base class without preceding modifiers.
*
* @example 'bg-gray-100' // for `hover:dark:bg-gray-100`
*/
baseClassName: string
/**
* Index position of a possible postfix modifier in the class.
* If the class has no postfix modifier, this is `undefined`.
*
* This property is prefixed with "maybe" because tailwind-merge does not know whether something is a postfix modifier or part of the base class since it's possible to configure Tailwind CSS classes which include a `/` in the base class name.
*
* If a `maybePostfixModifierPosition` is present, tailwind-merge first tries to match the `baseClassName` without the possible postfix modifier to a class group. If tht fails, it tries again with the possible postfix modifier.
*
* @example 11 // for `bg-gray-100/50`
*/
maybePostfixModifierPosition: number | undefined
}
interface ConfigGroups<ClassGroupIds extends string, ThemeGroupIds extends string> {

@@ -30,0 +80,0 @@ /**

@@ -13,63 +13,40 @@ const arbitraryValueRegex = /^\[(?:([a-z-]+):)?(.+)\]$/i

export function isLength(value: string) {
return isNumber(value) || stringLengths.has(value) || fractionRegex.test(value)
}
export const isLength = (value: string) =>
isNumber(value) || stringLengths.has(value) || fractionRegex.test(value)
export function isArbitraryLength(value: string) {
return getIsArbitraryValue(value, 'length', isLengthOnly)
}
export const isArbitraryLength = (value: string) =>
getIsArbitraryValue(value, 'length', isLengthOnly)
export function isNumber(value: string) {
return Boolean(value) && !Number.isNaN(Number(value))
}
export const isNumber = (value: string) => Boolean(value) && !Number.isNaN(Number(value))
export function isArbitraryNumber(value: string) {
return getIsArbitraryValue(value, 'number', isNumber)
}
export const isArbitraryNumber = (value: string) => getIsArbitraryValue(value, 'number', isNumber)
export function isInteger(value: string) {
return Boolean(value) && Number.isInteger(Number(value))
}
export const isInteger = (value: string) => Boolean(value) && Number.isInteger(Number(value))
export function isPercent(value: string) {
return value.endsWith('%') && isNumber(value.slice(0, -1))
}
export const isPercent = (value: string) => value.endsWith('%') && isNumber(value.slice(0, -1))
export function isArbitraryValue(value: string) {
return arbitraryValueRegex.test(value)
}
export const isArbitraryValue = (value: string) => arbitraryValueRegex.test(value)
export function isTshirtSize(value: string) {
return tshirtUnitRegex.test(value)
}
export const isTshirtSize = (value: string) => tshirtUnitRegex.test(value)
const sizeLabels = new Set(['length', 'size', 'percentage'])
export function isArbitrarySize(value: string) {
return getIsArbitraryValue(value, sizeLabels, isNever)
}
export const isArbitrarySize = (value: string) => getIsArbitraryValue(value, sizeLabels, isNever)
export function isArbitraryPosition(value: string) {
return getIsArbitraryValue(value, 'position', isNever)
}
export const isArbitraryPosition = (value: string) =>
getIsArbitraryValue(value, 'position', isNever)
const imageLabels = new Set(['image', 'url'])
export function isArbitraryImage(value: string) {
return getIsArbitraryValue(value, imageLabels, isImage)
}
export const isArbitraryImage = (value: string) => getIsArbitraryValue(value, imageLabels, isImage)
export function isArbitraryShadow(value: string) {
return getIsArbitraryValue(value, '', isShadow)
}
export const isArbitraryShadow = (value: string) => getIsArbitraryValue(value, '', isShadow)
export function isAny() {
return true
}
export const isAny = () => true
function getIsArbitraryValue(
const getIsArbitraryValue = (
value: string,
label: string | Set<string>,
testValue: (value: string) => boolean,
) {
) => {
const result = arbitraryValueRegex.exec(value)

@@ -88,19 +65,12 @@

function isLengthOnly(value: string) {
const isLengthOnly = (value: string) =>
// `colorFunctionRegex` check is necessary because color functions can have percentages in them which which would be incorrectly classified as lengths.
// For example, `hsl(0 0% 0%)` would be classified as a length without this check.
// I could also use lookbehind assertion in `lengthUnitRegex` but that isn't supported widely enough.
return lengthUnitRegex.test(value) && !colorFunctionRegex.test(value)
}
lengthUnitRegex.test(value) && !colorFunctionRegex.test(value)
function isNever() {
return false
}
const isNever = () => false
function isShadow(value: string) {
return shadowRegex.test(value)
}
const isShadow = (value: string) => shadowRegex.test(value)
function isImage(value: string) {
return imageRegex.test(value)
}
const isImage = (value: string) => imageRegex.test(value)

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc