From 6fdf5efe25c908e59bd64a140a9707babfab626f Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Sun, 7 Jul 2024 14:47:05 +0200 Subject: [PATCH 1/8] rename class utils to class group utils --- src/lib/{class-utils.ts => class-group-utils.ts} | 2 +- src/lib/config-utils.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/lib/{class-utils.ts => class-group-utils.ts} (99%) diff --git a/src/lib/class-utils.ts b/src/lib/class-group-utils.ts similarity index 99% rename from src/lib/class-utils.ts rename to src/lib/class-group-utils.ts index 4688690a..66ec8ac2 100644 --- a/src/lib/class-utils.ts +++ b/src/lib/class-group-utils.ts @@ -22,7 +22,7 @@ interface ClassValidatorObject { const CLASS_PART_SEPARATOR = '-' -export function createClassUtils(config: GenericConfig) { +export function createClassGroupUtils(config: GenericConfig) { const classMap = createClassMap(config) const { conflictingClassGroups, conflictingClassGroupModifiers } = config diff --git a/src/lib/config-utils.ts b/src/lib/config-utils.ts index 59a7ab23..d7fdc454 100644 --- a/src/lib/config-utils.ts +++ b/src/lib/config-utils.ts @@ -1,4 +1,4 @@ -import { createClassUtils } from './class-utils' +import { createClassGroupUtils } from './class-group-utils' import { createLruCache } from './lru-cache' import { createSplitModifiers } from './modifier-utils' import { GenericConfig } from './types' @@ -9,6 +9,6 @@ export function createConfigUtils(config: GenericConfig) { return { cache: createLruCache(config.cacheSize), splitModifiers: createSplitModifiers(config), - ...createClassUtils(config), + ...createClassGroupUtils(config), } } From f39111d0969c296bc6b18cdc7118374991538724 Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Sun, 7 Jul 2024 14:49:11 +0200 Subject: [PATCH 2/8] rename splitModifiers to parseClassName --- src/lib/config-utils.ts | 4 ++-- src/lib/merge-classlist.ts | 6 +++--- src/lib/{modifier-utils.ts => parse-class-name.ts} | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) rename src/lib/{modifier-utils.ts => parse-class-name.ts} (94%) diff --git a/src/lib/config-utils.ts b/src/lib/config-utils.ts index d7fdc454..b39a2261 100644 --- a/src/lib/config-utils.ts +++ b/src/lib/config-utils.ts @@ -1,6 +1,6 @@ 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' export type ConfigUtils = ReturnType @@ -8,7 +8,7 @@ export type ConfigUtils = ReturnType export function createConfigUtils(config: GenericConfig) { return { cache: createLruCache(config.cacheSize), - splitModifiers: createSplitModifiers(config), + parseClassName: createParseClassName(config), ...createClassGroupUtils(config), } } diff --git a/src/lib/merge-classlist.ts b/src/lib/merge-classlist.ts index 75a8d75c..fa18139a 100644 --- a/src/lib/merge-classlist.ts +++ b/src/lib/merge-classlist.ts @@ -1,10 +1,10 @@ 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 function mergeClassList(classList: string, configUtils: ConfigUtils) { - const { splitModifiers, getClassGroupId, getConflictingClassGroupIds } = configUtils + const { parseClassName, getClassGroupId, getConflictingClassGroupIds } = configUtils /** * Set of classGroupIds in following format: @@ -25,7 +25,7 @@ export function mergeClassList(classList: string, configUtils: ConfigUtils) { hasImportantModifier, baseClassName, maybePostfixModifierPosition, - } = splitModifiers(originalClassName) + } = parseClassName(originalClassName) let classGroupId = getClassGroupId( maybePostfixModifierPosition diff --git a/src/lib/modifier-utils.ts b/src/lib/parse-class-name.ts similarity index 94% rename from src/lib/modifier-utils.ts rename to src/lib/parse-class-name.ts index 7709422d..beeded99 100644 --- a/src/lib/modifier-utils.ts +++ b/src/lib/parse-class-name.ts @@ -2,14 +2,14 @@ import { GenericConfig } from './types' export const IMPORTANT_MODIFIER = '!' -export function createSplitModifiers(config: GenericConfig) { +export function createParseClassName(config: GenericConfig) { const separator = config.separator const isSeparatorSingleCharacter = separator.length === 1 const firstSeparatorCharacter = separator[0] const separatorLength = separator.length - // splitModifiers inspired by https://github.com/tailwindlabs/tailwindcss/blob/v3.2.2/src/util/splitAtTopLevelOnly.js - return function splitModifiers(className: string) { + // parseClassName inspired by https://github.com/tailwindlabs/tailwindcss/blob/v3.2.2/src/util/splitAtTopLevelOnly.js + return function parseClassName(className: string) { const modifiers = [] let bracketDepth = 0 From e28c73af14389ea74bb38b0e99931dbf1bf19861 Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Sun, 7 Jul 2024 15:08:02 +0200 Subject: [PATCH 3/8] add experimentalParseClassName feature to tailwind-merge --- src/index.ts | 2 ++ src/lib/parse-class-name.ts | 12 ++++++++++-- src/lib/types.ts | 28 ++++++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 25d16cb2..b9873ab5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,5 +10,7 @@ export { type Config, type DefaultClassGroupIds, type DefaultThemeGroupIds, + type ExperimentalParseClassNameParam, + type ExperimentalParsedClassName, } from './lib/types' export * as validators from './lib/validators' diff --git a/src/lib/parse-class-name.ts b/src/lib/parse-class-name.ts index beeded99..51b2e734 100644 --- a/src/lib/parse-class-name.ts +++ b/src/lib/parse-class-name.ts @@ -3,13 +3,13 @@ import { GenericConfig } from './types' export const IMPORTANT_MODIFIER = '!' export function createParseClassName(config: GenericConfig) { - const separator = config.separator + const { separator, experimentalParseClassName } = config const isSeparatorSingleCharacter = separator.length === 1 const firstSeparatorCharacter = separator[0] const separatorLength = separator.length // parseClassName inspired by https://github.com/tailwindlabs/tailwindcss/blob/v3.2.2/src/util/splitAtTopLevelOnly.js - return function parseClassName(className: string) { + function parseClassName(className: string) { const modifiers = [] let bracketDepth = 0 @@ -63,6 +63,14 @@ export function createParseClassName(config: GenericConfig) { maybePostfixModifierPosition, } } + + if (experimentalParseClassName) { + return function parseClassNameExperimental(className: string) { + return experimentalParseClassName({ className, parseClassName }) + } + } + + return parseClassName } /** diff --git a/src/lib/types.ts b/src/lib/types.ts index 6d75fbdf..def9f599 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -19,6 +19,34 @@ interface ConfigStatic { * @see https://tailwindcss.com/docs/configuration#separator */ separator: string + /** + * Allows to customize parsing of individual class names. + * + * This is an experimental feature and may introduce breaking changes in any minor version update. + */ + experimentalParseClassName?(param: ExperimentalParseClassNameParam): ExperimentalParsedClassName +} + +/** + * Type of param passed to 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 experimentalParseClassName function. + * + * This is an experimental feature and may introduce breaking changes in any minor version update. + */ +export interface ExperimentalParsedClassName { + modifiers: string[] + hasImportantModifier: boolean + baseClassName: string + maybePostfixModifierPosition: number | undefined } interface ConfigGroups { From 91eb1b640e2e026a3a362d36a703e7b3d7e1d5a6 Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Sun, 7 Jul 2024 15:43:12 +0200 Subject: [PATCH 4/8] add inline documentation for `experimentalParseClassName` --- src/lib/merge-classlist.ts | 7 +++---- src/lib/types.ts | 32 +++++++++++++++++++++++++++++--- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/src/lib/merge-classlist.ts b/src/lib/merge-classlist.ts index fa18139a..de1142ba 100644 --- a/src/lib/merge-classlist.ts +++ b/src/lib/merge-classlist.ts @@ -27,16 +27,15 @@ export function mergeClassList(classList: string, configUtils: ConfigUtils) { maybePostfixModifierPosition, } = parseClassName(originalClassName) + let hasPostfixModifier = Boolean(maybePostfixModifierPosition) let classGroupId = getClassGroupId( - maybePostfixModifierPosition + hasPostfixModifier ? baseClassName.substring(0, maybePostfixModifierPosition) : baseClassName, ) - let hasPostfixModifier = Boolean(maybePostfixModifierPosition) - if (!classGroupId) { - if (!maybePostfixModifierPosition) { + if (!hasPostfixModifier) { return { isTailwindClass: false as const, originalClassName, diff --git a/src/lib/types.ts b/src/lib/types.ts index def9f599..b79cf84a 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -20,7 +20,8 @@ interface ConfigStatic { */ separator: string /** - * Allows to customize parsing of individual class names. + * 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. */ @@ -28,7 +29,7 @@ interface ConfigStatic { } /** - * Type of param passed to experimentalParseClassName function. + * Type of param passed to the `experimentalParseClassName` function. * * This is an experimental feature and may introduce breaking changes in any minor version update. */ @@ -38,14 +39,39 @@ export interface ExperimentalParseClassNameParam { } /** - * Type of the result returned by experimentalParseClassName function. + * 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 } From f6c7b19c5ef11ea030308b2c0ea7918710407f95 Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Sun, 7 Jul 2024 15:55:11 +0200 Subject: [PATCH 5/8] update versioning docs --- docs/versioning.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/versioning.md b/docs/versioning.md index d4a9ae89..124e523b 100644 --- a/docs/versioning.md +++ b/docs/versioning.md @@ -8,11 +8,13 @@ This package follows the [SemVer](https://semver.org) versioning rules. More spe - Major version gets incremented when breaking changes are introduced to the package API. E.g. the return type of `twMerge` changes. -- `alpha` releases might introduce breaking changes on any update. Whereas `beta` releases only introduce new features or bug fixes. +- `alpha` releases might introduce breaking changes on any update. `beta` releases intend to only introduce new features or bug fixes, but can introduce breaking changes in rare cases. + +- Any API that has `experimental` in its name can introduce breaking changes in any minor version update. - Releases with major version 0 might introduce breaking changes on a minor version update. -- A non-production-ready version of every commit pushed to the main branch is released under the `dev` tag for testing purposes. It has a format like [`1.6.1-dev.4202ccf913525617f19fbc493db478a76d64d054`](https://www.npmjs.com/package/tailwind-merge/v/1.6.1-dev.4202ccf913525617f19fbc493db478a76d64d054) in which the first numbers are the corresponding last release and the hash at the end is the git SHA of the commit. You can install the latest dev release with `yarn add tailwind-merge@dev`. +- A non-production-ready version of every commit pushed to the main branch is released under the `dev` tag for testing purposes. It has a format like [`1.6.1-dev.4202ccf913525617f19fbc493db478a76d64d054`](https://www.npmjs.com/package/tailwind-merge/v/1.6.1-dev.4202ccf913525617f19fbc493db478a76d64d054) in which the first numbers are the corresponding last release and the hash at the end is the git SHA of the commit. You can install the latest dev release with `npm install tailwind-merge@dev`. - A changelog is documented in [GitHub Releases](https://github.com/dcastil/tailwind-merge/releases). From 922bfdac14f8a8f08704d859552c085c733846c3 Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Sun, 7 Jul 2024 17:55:36 +0200 Subject: [PATCH 6/8] fix experimentalParseClassName config property not being overriden in mergeConfigs --- src/lib/merge-configs.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib/merge-configs.ts b/src/lib/merge-configs.ts index df2d765f..94c8b39c 100644 --- a/src/lib/merge-configs.ts +++ b/src/lib/merge-configs.ts @@ -10,6 +10,7 @@ export function mergeConfigs, @@ -17,6 +18,7 @@ export function mergeConfigs Date: Sun, 7 Jul 2024 17:55:54 +0200 Subject: [PATCH 7/8] fix outdated import path in class-map test --- tests/class-map.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/class-map.test.ts b/tests/class-map.test.ts index 0dd5eb82..f1f1c120 100644 --- a/tests/class-map.test.ts +++ b/tests/class-map.test.ts @@ -1,5 +1,5 @@ import { getDefaultConfig } from '../src' -import { ClassPartObject, createClassMap } from '../src/lib/class-utils' +import { ClassPartObject, createClassMap } from '../src/lib/class-group-utils' test('class map has correct class groups at first part', () => { const classMap = createClassMap(getDefaultConfig()) From 9aa1c8e80e055a196b55e4522ea2d733cbdf31e5 Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Sun, 7 Jul 2024 18:05:54 +0200 Subject: [PATCH 8/8] add tests for experimentalParseClassName --- tests/experimental-parse-class-name.test.ts | 37 +++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 tests/experimental-parse-class-name.test.ts diff --git a/tests/experimental-parse-class-name.test.ts b/tests/experimental-parse-class-name.test.ts new file mode 100644 index 00000000..889b0c43 --- /dev/null +++ b/tests/experimental-parse-class-name.test.ts @@ -0,0 +1,37 @@ +import { extendTailwindMerge } from '../src' + +test('default case', () => { + const twMerge = extendTailwindMerge({ + experimentalParseClassName({ className, parseClassName }) { + return parseClassName(className) + }, + }) + + expect(twMerge('px-2 py-1 p-3')).toBe('p-3') +}) + +test('removing first three characters from class', () => { + const twMerge = extendTailwindMerge({ + experimentalParseClassName({ className, parseClassName }) { + return parseClassName(className.slice(3)) + }, + }) + + expect(twMerge('barpx-2 foopy-1 lolp-3')).toBe('lolp-3') +}) + +test('ignoring breakpoint modifiers', () => { + const breakpoints = new Set(['sm', 'md', 'lg', 'xl', '2xl']) + const twMerge = extendTailwindMerge({ + experimentalParseClassName({ className, parseClassName }) { + const parsed = parseClassName(className) + + return { + ...parsed, + modifiers: parsed.modifiers.filter((modifier) => !breakpoints.has(modifier)), + } + }, + }) + + expect(twMerge('md:px-2 hover:py-4 py-1 lg:p-3')).toBe('hover:py-4 lg:p-3') +})