pax_global_header00006660000000000000000000000064152000777220014514gustar00rootroot0000000000000052 comment=d54f7e5713c653d0171971405344f7c6e44d418f dcastil-tailwind-merge-c8c57aa/000077500000000000000000000000001520007772200165425ustar00rootroot00000000000000dcastil-tailwind-merge-c8c57aa/.agents/000077500000000000000000000000001520007772200201015ustar00rootroot00000000000000dcastil-tailwind-merge-c8c57aa/.agents/release-workflow.md000066400000000000000000000114461520007772200237210ustar00rootroot00000000000000# Release workflow for agents Use this guide when preparing release changelog entries and GitHub release text. ## Scope - Applies to release preparation tasks like `v3.4.1`. - Covers: - Draft release ingestion from GitHub. - Changelog updates in `docs/changelog/*-changelog.md`. - Sponsor section generation and ordering. - GitHub Releases UI text formatting. - Release comments on PRs/issues via `.github/actions/release-commenter`. ## Inputs to collect first 1. Release tag and base compare range (for example `v3.4.0...v3.4.1`). 2. Draft release body from GitHub. 3. Current active GitHub sponsors. 4. Sponsor payout input from thanks.dev for the relevant time window. `thanks.dev` input must come from the user. Do not infer or invent this data. ## Release commenter behavior The workflow `.github/workflows/comment-released-prs-and-issues.yml` uses the local action `.github/actions/release-commenter`. - It runs for: - published GitHub releases, - manual workflow dispatch, - completed successful `npm Publish` runs triggered by `push` on `main` (dev-release comment pass). - Automatic base-tag selection is semver-aware: - Stable release tags compare to the previous stable semver tag. - Prerelease tags with a SHA suffix (for example `v3.4.1-dev.`) resolve base from npm-published versions with the same prerelease prefix and pick the nearest ancestor commit. - For SHA-suffixed prereleases, if no prior dev release exists for the same core version, resolution falls back to the highest semver lower than the current version (same prerelease prefix). - SHA-suffixed prerelease comments link to the npm published version page instead of GitHub release tags. - Other prerelease tags compare to the previous semver tag (including prereleases). - If no valid base tag is found, the action fails. - The action fails before posting if any target issue/PR already has a previous stable release-comment. - For prereleases, duplicate/previous-comment guard checks are skipped so `-dev.*` and later stable releases can both comment. - Manual trigger supports optional overrides: - `head_tag` - `base_tag` - `dry_run` - `npm_package_name` Manual dry run example: ```bash gh workflow run comment-released-prs-and-issues.yml \ -f head_tag=v3.4.1 \ -f dry_run=true ``` ## Commands Fetch draft release: ```bash gh release view --json tagName,name,isDraft,body,url,createdAt,targetCommitish ``` Fetch active GitHub sponsors in start-date order: ```bash gh api graphql -f query='query($login:String!){ user(login:$login){ sponsorshipsAsMaintainer(first:100, activeOnly:true, includePrivate:false, orderBy:{field:CREATED_AT, direction:ASC}){ nodes{ sponsorEntity{ ... on User{ login } ... on Organization{ login } } } } } }' -F login=dcastil ``` Check whether private active sponsors exist: ```bash gh api graphql -f query='query($login:String!){ user(login:$login){ public: sponsorshipsAsMaintainer(first:100, activeOnly:true, includePrivate:false){ totalCount } all: sponsorshipsAsMaintainer(first:100, activeOnly:true, includePrivate:true){ totalCount } } }' -F login=dcastil ``` ## Changelog authoring rules 1. Update the matching major-version changelog file: - v3 releases: `docs/changelog/v3-changelog.md` - v2 releases: `docs/changelog/v2-changelog.md` 2. Add new version section at the top: - `## vX.Y.Z` 3. Keep category headings from draft release: - `### Bug Fixes`, `### New Features`, `### Documentation`, `### Other`, etc. 4. Keep full compare link in docs style: - `**Full Changelog**: [\`vA.B.C...vX.Y.Z\`](https://github.com/dcastil/tailwind-merge/compare/vA.B.C...vX.Y.Z)` ## Sponsor rules 1. Preserve ordering convention from the most recent changelog sponsor sentence unless user explicitly asks otherwise. 2. Merge sponsor sources: - GitHub Sponsors (active sponsors, ordered by start date). - User-provided thanks.dev payouts for the release window. 3. Include thanks.dev sponsors only above the user-defined threshold. - Current default: include only amounts greater or equal to 1 USD. 4. Do not mention sub-threshold sponsors individually unless requested. 5. Keep summary phrases aligned with user preference: - Examples: `a private sponsor`, `and more via @thnxdev`. ## GitHub release body formatting When producing text for GitHub Releases UI, transform docs formatting: 1. Keep headings and bullet content. 2. Convert markdown profile links to plain handles: - `[@name](https://github.com/name)` -> `@name` 3. Convert markdown PR links to plain URLs. 4. Use plain compare URL: - `**Full Changelog**: https://github.com/dcastil/tailwind-merge/compare/vA.B.C...vX.Y.Z` ## Final output contract 1. Provide the release body in one copy-paste-ready markdown block. 2. If sponsor payout input is missing, stop and ask the user for thanks.dev data before finalizing sponsor lines. dcastil-tailwind-merge-c8c57aa/.agents/tailwind-css-version-update.md000066400000000000000000000211241520007772200257670ustar00rootroot00000000000000# Tailwind CSS version update workflow Use this guide when updating tailwind-merge for a new Tailwind CSS version. The main risk in this work is false confidence: Tailwind release notes, deployed docs, source code, and website docs can land at different times. Treat every feature as something to verify from official sources before changing `src/lib/default-config.ts` or parser behavior. ## Official sources - Tailwind CSS releases: https://github.com/tailwindlabs/tailwindcss/releases - Tailwind CSS source and PRs: https://github.com/tailwindlabs/tailwindcss - Tailwind CSS release tag pattern: `https://github.com/tailwindlabs/tailwindcss/releases/tag/vX.Y.Z` - Tailwind CSS deployed docs: https://tailwindcss.com/docs - Tailwind CSS website source and docs PRs: https://github.com/tailwindlabs/tailwindcss.com - Prior tailwind-merge support PRs for comparison, for example v4.2 support: https://github.com/dcastil/tailwind-merge/pull/651 When docs for a just-released Tailwind version are not deployed yet, use the `tailwindcss.com` repository and its PRs to find future docs pages, sidebar order, and link targets. ## Investigation checklist 1. Read the Tailwind CSS release notes for the target version. 2. Open every linked Tailwind CSS PR that mentions utilities, variants, parser syntax, arbitrary values, or behavior changes. 3. Inspect Tailwind CSS source and tests when release notes or PR descriptions are ambiguous. Do not infer support for arbitrary values or variables from naming patterns alone. 4. Check the `tailwindcss.com` repository for docs PRs when deployed docs lag behind the release. 5. Compare against the previous tailwind-merge Tailwind-support PR to reuse the same structure and testing style. 6. Make a feature list before coding. Split work into simple utility additions, ordering/docs updates, conflict changes, and parser/config API changes. 7. Record no-op decisions in the PR or issue summary, especially when a Tailwind PR looked relevant but does not require a tailwind-merge change. ## Triage questions for each Tailwind change - Does it emit new class names? If yes, it likely needs `classGroups` entries. - Does it change which utilities conflict? If yes, update `conflictingClassGroups` or `conflictingClassGroupModifiers`. - Does it add a new arbitrary value or arbitrary variable form? Verify the exact accepted syntax in Tailwind source before adding validators. - Does it add a slash form that is part of the base class rather than a postfix modifier? Prefer config-driven lookup such as `postfixLookupClassGroups` before changing parser logic. - Does it change modifier, variant, prefix, important, or arbitrary-variant syntax? Parser changes are higher risk and need focused parser/modifier tests. - Does it only change generated CSS for an existing class without changing class syntax or conflicts? It may not require a tailwind-merge change. - Does it remove or rename classes? Check whether keeping old classes would create incorrect merges for the supported Tailwind range. ## Default config ordering and links Keep `src/lib/default-config.ts` ordered according to the Tailwind CSS docs sidebar, not alphabetically and not by implementation convenience. For new utility groups: - Use `tailwindcss.com` or the website repository to find the final docs URL for each `@see` comment. - If the public docs are not deployed, inspect docs PRs in `tailwindlabs/tailwindcss.com`. - Replace temporary `TBD` links before finishing the change. - Keep related utility groups in sidebar order even when they share a prefix. There is an important exception to sidebar order: class-group order affects validator precedence for groups that share the same prefix path. The class map tries exact child paths first, then runs validators in the order they were registered. If a broad validator appears before a more specific validator for the same prefix, the broad group can claim the class before the specific group sees it. When groups with the same prefix use validators: - Put the most specific validators first. - Put broad or catch-all validators last for that prefix. - Treat color validators as especially sensitive because they often accept arbitrary values or otherwise broad value sets. - Prefer correctness over strict sidebar order inside that shared-prefix cluster. For example, if a `text-*` color group uses a broad color validator, keep it after more specific `text-*` groups such as font-size-related groups so ambiguous classes resolve to the intended class group. Examples learned during the Tailwind CSS v4.3 update: - `tab-size` belongs after `text-indent` and before `vertical-align`. - `zoom` belongs after `translate`. - Scrollbar groups should follow the sidebar order for scrollbar color, scrollbar gutter, and scrollbar width. - PRs that look relevant to existing utilities, such as changes around `start` and `end`, still need source review before changing tailwind-merge. ## Implementation guidance Use `src/lib/default-config.ts` as the behavioral source of truth for utilities and conflicts. Prefer config changes over parser changes. Parser and merge hot paths run frequently in UI renders, so keep extra work small and opt-in: - Normalize config arrays into lookup objects in `createConfigUtils` when they need fast querying. - Avoid scanning or balancing brackets in validators when a cheap prefix or character check can reject most inputs. - Put specialized syntax checks in `src/lib/validators.ts` when they are reusable or need direct tests. - Add comments only for non-obvious behavior, especially when a config entry compensates for parser ambiguity. For slash syntax, remember that tailwind-merge initially treats the part after `/` as a possible postfix modifier. If the full class can belong to a different group, use a generic config mechanism rather than a syntax-specific parser branch. The Tailwind CSS v4.3 named container query support is the reference pattern: - `@container-size` is a container type class. - `@container-size/sidebar` combines container type and container name. - The parser already exposes a possible postfix modifier, so no bracket-counting parser change is needed. - `container-named` creates a conflict with `container-type`, but not the reverse, because a later type-only class should preserve the earlier container name. - `postfixLookupClassGroups: ['container-type']` tells merge logic to try resolving the full slash class only after the pre-slash class resolves to `container-type`. ## Testing checklist Add or update tests based on the kind of change: - Tailwind version compatibility: `tests/tailwind-css-versions.test.ts` - Default config class map coverage: `tests/class-map.test.ts` - Class group conflicts: `tests/class-group-conflicts.test.ts` and `tests/conflicts-across-class-groups.test.ts` - Parser or modifier semantics: `tests/modifiers.test.ts`, `tests/arbitrary-variants.test.ts`, `tests/experimental-parse-class-name.test.ts` - Validators: `tests/validators.test.ts` - Public exports and config API: `tests/public-api.test.ts`, `tests/type-generics.test.ts`, `tests/merge-configs.test.ts` - Docs examples: `tests/docs-examples.test.ts` For new utility support, include tests for: - Last class wins within the new class group. - Cross-group conflicts in both orders when conflicts are asymmetric. - Arbitrary values and arbitrary variables only when Tailwind source confirms support. - Important modifier and variant combinations when parser or conflict behavior is involved. - Unknown or invalid classes remaining untouched. For TypeScript config changes, remember that `yarn tsc --noEmit` only checks `src`. Use the test tsconfig as an additional check when touching public types: ```sh yarn tsc --noEmit -p tests/tsconfig.json --moduleResolution bundler ``` The plain `tests/tsconfig.json` currently inherits `moduleResolution: Node`, which can surface Vitest package export resolution errors unrelated to tailwind-merge types. ## Documentation checklist Update docs in the same change when behavior or public API changes: - `docs/configuration.md` for config behavior. - `docs/api-reference.md` for exported validators, config fields, or public types. - `docs/changelog/v3-changelog.md` when preparing a release note. - `AGENTS.md` and `.agents/*` when the update process or repo guidance changes. Do not regenerate `README.md` during normal development; it is generated from `docs/README.md` during the release flow. ## Validation checklist Run targeted tests while iterating, then run the full validation set before finalizing: ```sh yarn lint yarn tsc --noEmit yarn test git diff --check ``` Run `yarn build && yarn test:exports` when touching package exports, build tooling, or generated declaration behavior. dcastil-tailwind-merge-c8c57aa/.agents/tailwind-merge-internals.md000066400000000000000000000111261520007772200253310ustar00rootroot00000000000000# tailwind-merge internals guide This file is a practical map for agent-driven changes in this repo. ## How merge execution works 1. `twMerge` in `src/lib/tw-merge.ts` is created via `createTailwindMerge(getDefaultConfig)`. 2. `createTailwindMerge` in `src/lib/create-tailwind-merge.ts`: - lazily builds config utils on first call, - joins inputs via `twJoin`, - caches final joined-input results, - calls `mergeClassList` on cache miss. 3. `mergeClassList` in `src/lib/merge-classlist.ts`: - parses classes, - walks from right to left so "last class wins", - resolves class group IDs and asymmetric conflicts, - keeps non-Tailwind/external classes untouched. 4. `createClassGroupUtils` in `src/lib/class-group-utils.ts`: - compiles class definitions into a recursive map + validator list, - maps class names to class group IDs, - returns conflicting groups, including postfix-modifier conflicts. 5. `createParseClassName` in `src/lib/parse-class-name.ts`: - parses stacked modifiers with bracket/paren depth tracking, - supports important modifier (`!`) and legacy v3 position, - supports optional prefix and `experimentalParseClassName` override. 6. `createSortModifiers` in `src/lib/sort-modifiers.ts`: - alphabetically sorts non-sensitive modifiers, - preserves placement around arbitrary/order-sensitive modifiers. ## Files to edit by intent - Add or change Tailwind utility support: - `src/lib/default-config.ts` - Usually paired with tests in `tests/tailwind-css-versions.test.ts` and focused utility tests. - For a full Tailwind CSS version update, follow `.agents/tailwind-css-version-update.md`. - Change conflict behavior: - `src/lib/default-config.ts` (`conflictingClassGroups`, `conflictingClassGroupModifiers`) - `tests/class-group-conflicts.test.ts` - `tests/conflicts-across-class-groups.test.ts` - Change parser/modifier behavior: - `src/lib/parse-class-name.ts` - `src/lib/sort-modifiers.ts` - `tests/modifiers.test.ts` - `tests/arbitrary-variants.test.ts` - `tests/experimental-parse-class-name.test.ts` - Change config extension APIs: - `src/lib/extend-tailwind-merge.ts` - `src/lib/create-tailwind-merge.ts` - `src/lib/merge-configs.ts` - `tests/create-tailwind-merge.test.ts` - `tests/extend-tailwind-merge.test.ts` - `tests/merge-configs.test.ts` - `tests/type-generics.test.ts` - Change validators: - `src/lib/validators.ts` - `tests/validators.test.ts` - Any impacted arbitrary-value tests ## Testing strategy in this repo - Unit/behavior tests are granular by feature area in `tests/*.test.ts`. - `tests/tailwind-css-versions.test.ts` is the compatibility anchor for Tailwind feature coverage by version. - `tests/docs-examples.test.ts` enforces that documented `twMerge(...) // -> ...` examples stay correct. - `tests/public-api.test.ts` guards runtime exports and broad type usage. - `tests/tw-merge.benchmark.ts` is for performance benchmarking (`yarn bench`), not correctness gating in CI. Recommended local sequence for non-trivial changes: 1. `yarn lint` 2. `yarn test` 3. `yarn build` 4. `yarn test:exports` ## Build, packaging, and release notes - Build uses Rollup (`scripts/rollup.config.mjs`) to produce: - ESM and CJS bundles, - ES5 variants, - unified type declarations. - Export smoke tests: - `scripts/test-built-package-exports.cjs` - `scripts/test-built-package-exports.mjs` - `README.md` is generated from `docs/README.md` by `scripts/update-readme.mjs` (run in `version` script via `zx`). ### Release notes playbook Detailed release-specific agent workflow lives in: - `.agents/release-workflow.md` ## CI behavior (what must stay green) - `.github/workflows/test.yml` runs `yarn lint`, `yarn test`, `yarn build`, and `yarn test:exports`. - `.github/workflows/benchmark.yml` runs `yarn bench` with CodSpeed. - `.github/workflows/npm-publish.yml`: - publishes `dev` tag on `main` pushes, - publishes production on release events. - `.github/workflows/comment-released-prs-and-issues.yml`: - runs local action `.github/actions/release-commenter`, - runs on `release.published`, on manual `workflow_dispatch`, and after successful `npm Publish` workflow completion for `push` events on `main`, - supports manual `workflow_dispatch` with optional `head_tag`, `base_tag`, `dry_run`, and `npm_package_name`. ## Practical guardrails - Keep `README.md` edits synchronized with `docs/README.md`; source-of-truth is docs. - Do not rely on `dist/` as edit target; regenerate instead. - Favor targeted tests while iterating, then run full suite before finalizing. - Any behavior change should include at least one regression test. dcastil-tailwind-merge-c8c57aa/.github/000077500000000000000000000000001520007772200201025ustar00rootroot00000000000000dcastil-tailwind-merge-c8c57aa/.github/CODE_OF_CONDUCT.md000066400000000000000000000017531520007772200227070ustar00rootroot00000000000000# tailwind-merge Community Guidelines The following community guidelines are based on [The Ruby Community Conduct Guidelines](https://www.ruby-lang.org/en/conduct) which are also used by [Tailwind CSS](https://github.com/tailwindlabs/tailwindcss/blob/master/.github/CODE_OF_CONDUCT.md). This document provides community guidelines for a safe, respectful, productive, and collaborative place for any person who is willing to contribute to the tailwind-merge project. It applies to all “collaborative space”, which is defined as community communications channels (such as issues, discussions, pull requests, commit comments, etc.). - Participants will be tolerant of opposing views. - Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks. - When interpreting the words and actions of others, participants should always assume good intentions. - Behaviour which can be reasonably considered harassment will not be tolerated. dcastil-tailwind-merge-c8c57aa/.github/CONTRIBUTING.md000066400000000000000000000056171520007772200223440ustar00rootroot00000000000000# Contributing Contributions are welcome and will be fully credited. Please take a moment to review this document before creating an issue or pull request. It is based on the [Tiptap contributing guidelines](https://github.com/ueberdosis/tiptap/blob/main/CONTRIBUTING.md). ## Etiquette This project is open source, and as such, the maintainers give their free time to build and maintain the source code held within. They make the code freely available in the hope that it will be of use to other developers. It would be extremely unfair for them to suffer abuse or anger for their hard work. Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the world that developers are civilized and selfless people. It's the duty of the maintainer to ensure that all submissions to the project are of sufficient quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used. See [CODE OF CONDUCT](CODE_OF_CONDUCT.md) for more info. ## Viability When requesting or submitting new features, first consider whether it might be useful to others. Open source projects are used by many developers, who may have entirely different needs to your own. Think about whether or not your feature is likely to be used by other users of the project. ## Procedure Before filing an issue: - Attempt to replicate the problem, to ensure that it wasn't a coincidental incident. - Check to make sure your feature suggestion isn't already present within the project. - Check the pull requests tab to ensure that the bug doesn't have a fix in progress. - Check the pull requests tab to ensure that the feature isn't already in progress. Before submitting a pull request: - Check the codebase to ensure that your feature doesn't already exist. - Check the pull requests to ensure that another person hasn't already submitted the feature or fix. ## Requirements - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](https://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. ## Development You will need [Node.js](https://nodejs.org) and [yarn](https://classic.yarnpkg.com) installed on your machine. I recommend running tests in watch mode while you work on the code. Then the correct subset of tests is being run as you modify source code or the tests itself. ```sh # Install dependencies $ yarn # Run tests $ yarn test --watch ``` Happy coding! dcastil-tailwind-merge-c8c57aa/.github/ISSUE_TEMPLATE/000077500000000000000000000000001520007772200222655ustar00rootroot00000000000000dcastil-tailwind-merge-c8c57aa/.github/ISSUE_TEMPLATE/bug-report.md000066400000000000000000000013161520007772200246760ustar00rootroot00000000000000--- name: Bug Report about: Report that something isn't working as expected in tailwind-merge --- ### Describe the bug ### To Reproduce ### Expected behavior ### Environment - tailwind-merge version: [e.g. 1.13.1] ### Additional context dcastil-tailwind-merge-c8c57aa/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000011341520007772200242540ustar00rootroot00000000000000blank_issues_enabled: true contact_links: - name: Get Help url: https://github.com/dcastil/tailwind-merge/discussions/new?category=help about: If you can't get something to work the way you expect, open a question in the discussion forums. - name: Feature Request url: https://github.com/dcastil/tailwind-merge/discussions/new?category=ideas about: Suggest any ideas you have using the discussion forum. - name: Start discussion url: https://github.com/dcastil/tailwind-merge/discussions about: Anything else on your mind? Check out the discussions forum. dcastil-tailwind-merge-c8c57aa/.github/SECURITY.md000066400000000000000000000022121520007772200216700ustar00rootroot00000000000000# Security Policy ## Supported Versions Security updates are available for the two latest major versions. In the event of a security vulnerability in tailwind-merge, a patch release with a fix will be made to all affected latest major versions. I.e. if the two latest major versions of tailwind-merge would be `v9.3.4` and `v8.10.0` and a security vulnerability would get discovered which affected all versions from `v6.0.0` to `v9.3.4`, then at least the releases `v9.3.5` and `v8.10.1` would be made to fix the security vulnerability. ## Reporting a Vulnerability Please report vulnerabilities privately via GitHub at https://github.com/dcastil/tailwind-merge/security. In case it is not possible to report a vulnerability via GitHub, you can send me an email to metro_comical_03@icloud.com. However, I might change or disable this email address at any time depending on how much spam I get through it. You can expect an answer from me within 24 hours most of the time. However, if I'm travelling and don't have good reception, it could take up to a few days. Usually I set my GitHub status to busy when I expect to be unresponsive for more than a day. dcastil-tailwind-merge-c8c57aa/.github/actions/000077500000000000000000000000001520007772200215425ustar00rootroot00000000000000dcastil-tailwind-merge-c8c57aa/.github/actions/metrics-report/000077500000000000000000000000001520007772200245215ustar00rootroot00000000000000dcastil-tailwind-merge-c8c57aa/.github/actions/metrics-report/.gitignore000066400000000000000000000000051520007772200265040ustar00rootroot00000000000000temp dcastil-tailwind-merge-c8c57aa/.github/actions/metrics-report/action.yml000066400000000000000000000006041520007772200265210ustar00rootroot00000000000000name: 'metrics-report-action' author: 'Dany Castillo' description: 'Posts a comment with a report about changes in important metrics related to tailwind-merge' inputs: github_token: description: 'Github token of the repository (automatically created by Github)' default: '${{ github.token }}' required: false runs: using: 'node20' main: 'src/main.mjs' dcastil-tailwind-merge-c8c57aa/.github/actions/metrics-report/package.json000066400000000000000000000007051520007772200270110ustar00rootroot00000000000000{ "name": "metrics-report-action", "version": "0.1.0", "private": true, "author": "Dany Castillo", "scripts": { "start": "node src/main.mjs" }, "dependencies": { "@actions/core": "^3.0.1", "@actions/exec": "^3.0.0", "@actions/github": "^9.1.1", "@octokit/types": "^16.0.0", "@octokit/webhooks-types": "^7.6.1", "esbuild": "^0.28.0", "rollup": "^4.60.2" } } dcastil-tailwind-merge-c8c57aa/.github/actions/metrics-report/src/000077500000000000000000000000001520007772200253105ustar00rootroot00000000000000dcastil-tailwind-merge-c8c57aa/.github/actions/metrics-report/src/get-package-size.mjs000066400000000000000000000150121520007772200311420ustar00rootroot00000000000000// @ts-check import fs from 'fs/promises' import path from 'path' import { promisify } from 'util' import zlib from 'zlib' import * as core from '@actions/core' import { exec } from '@actions/exec' import { transform } from 'esbuild' import { rollup } from 'rollup' import { actionRootPath, repoRootPath } from './utils/path.mjs' /** * @typedef {object} GetPackageSizeOptions * @property {boolean=} shouldOmitFailures */ /** * @param {GetPackageSizeOptions=} options */ export async function getPackageSize(options = {}) { await buildPackage() return getEntryPointSizes(options) } async function buildPackage() { core.info('Installing dependencies') await exec('yarn install --frozen-lockfile', [], { cwd: repoRootPath }) core.info('Building package') await exec('yarn build', [], { cwd: repoRootPath }) } /** * @typedef {object} EntryPointSize * @property {BundleSize} bundleSize * @property {BundleSize[]=} singleExportSizes */ /** * @param {GetPackageSizeOptions} param0 * @returns {Promise} */ async function getEntryPointSizes({ shouldOmitFailures }) { core.info('Getting entry point configs') const entryPointConfigs = await getEntryPointConfigs() core.info('Getting bundle sizes') const maybeEntryPointSizes = await Promise.all( entryPointConfigs.map(async (entryPointConfig, entryPointIndex) => { const entryPointBundlePath = path.resolve(repoRootPath, entryPointConfig.bundlePath) const bundle = await getBundle(entryPointConfig, entryPointBundlePath).catch( (error) => { if (shouldOmitFailures) { core.info( `Failed to get bundle for ${entryPointConfig.entryPoint}: ${error.message}`, ) return } throw error }, ) if (!bundle) { return } const [bundleSize, singleExportSizes] = await Promise.all([ getBundleSize(entryPointConfig.entryPoint, bundle), getSingleExportBundleSizes( entryPointConfig, entryPointIndex, entryPointBundlePath, bundle, ), ]) return { bundleSize, singleExportSizes, } }), ) /** @type {any} */ const entryPointSizes = maybeEntryPointSizes.filter((bundleSize) => bundleSize !== undefined) return entryPointSizes } /** * @typedef {object} EntryPointConfiguration * @property {string} entryPoint * @property {string} bundlePath * @property {'esm' | 'cjs'} format */ /** * @returns {Promise} */ async function getEntryPointConfigs() { const pkg = (await import('../../../../package.json', { assert: { type: 'json' } })).default return Object.entries(pkg.exports).flatMap(([relativeEntryPointPath, bundleObject]) => { const entryPointPath = path.join('tailwind-merge', relativeEntryPointPath) /** @type {EntryPointConfiguration[]} */ const entryPointConfigs = [] if (bundleObject.import) { entryPointConfigs.push({ entryPoint: entryPointPath + ' esm', bundlePath: bundleObject.import, format: 'esm', }) } if (bundleObject.require) { entryPointConfigs.push({ entryPoint: entryPointPath + ' cjs', bundlePath: bundleObject.require, format: 'cjs', }) } return entryPointConfigs }) } /** * @param {EntryPointConfiguration} entryPointConfig * @param {string} entryPoint */ async function getBundle(entryPointConfig, entryPoint) { const rollupBuild = await rollup({ input: entryPoint }) let rollupOutput try { rollupOutput = await rollupBuild.generate({ format: entryPointConfig.format, }) } catch (error) { await rollupBuild.close() throw error } await rollupBuild.close() if (rollupOutput.output.length !== 1) { throw Error(`Expected a single output chunk for bundle ${entryPoint}`) } const outputChunk = rollupOutput.output[0] return outputChunk } /** * @param {EntryPointConfiguration} entryPointConfig * @param {number} entryPointIndex * @param {string} bundlePath * @param {import('rollup').OutputChunk} bundle */ async function getSingleExportBundleSizes(entryPointConfig, entryPointIndex, bundlePath, bundle) { if (entryPointConfig.format === 'esm' && bundle.exports.length !== 0) { const singleExportBundlesDirPath = path.resolve( actionRootPath, `temp/bundle-${entryPointIndex}`, ) await fs.mkdir(singleExportBundlesDirPath, { recursive: true }) return Promise.all( bundle.exports.map(async (exportName) => { const entryPoint = await createEntryPoint( singleExportBundlesDirPath, bundlePath, exportName, ) const singleExportBundle = await getBundle(entryPointConfig, entryPoint) return getBundleSize(exportName, singleExportBundle) }), ) } } /** * @param {string} singleExportBundlesDirPath * @param {string} bundlePath * @param {string} exportName */ async function createEntryPoint(singleExportBundlesDirPath, bundlePath, exportName) { const filePath = path.resolve(singleExportBundlesDirPath, `${exportName}.mjs`) const fileContent = `export { ${exportName} } from '${bundlePath}'` await fs.writeFile(filePath, fileContent) return filePath } /** * @typedef {object} BundleSize * @property {string} label * @property {number} size * @property {number} sizeMinified * @property {number} sizeBrotliCompressed */ /** * @param {string} label * @param {import('rollup').OutputChunk} bundle * @returns {Promise} */ async function getBundleSize(label, bundle) { const esBuildTransformResult = await transform(bundle.code, { minify: true }) const minifiedCode = esBuildTransformResult.code const brotliCompressedCode = (await brotliCompress(minifiedCode)).toString() return { label, size: bundle.code.length, sizeMinified: minifiedCode.length, sizeBrotliCompressed: brotliCompressedCode.length, } } const brotliCompress = promisify(zlib.brotliCompress) get-size-metrics-report-content.mjs000066400000000000000000000146021520007772200341230ustar00rootroot00000000000000dcastil-tailwind-merge-c8c57aa/.github/actions/metrics-report/src// @ts-check import { getDetails, getTableHtml, nonBreaking } from './utils/html.mjs' /** * @param {import('./get-package-size.mjs').EntryPointSize[]} entryPointSizesHead * @param {import('./get-package-size.mjs').EntryPointSize[]} entryPointSizesBase */ export function getSizeMetricsReportContent(entryPointSizesHead, entryPointSizesBase) { const entryPointSizeMetrics = getEntryPointSizeMetrics(entryPointSizesHead, entryPointSizesBase) const entryPointSizeChanges = entryPointSizeMetrics .filter(({ bundleSize, singleExportSizes }) => { return hasBundleSizeChange(bundleSize) || singleExportSizes?.some(hasBundleSizeChange) }) .map((entryPointSizeMetrics) => { if (!entryPointSizeMetrics.singleExportSizes) { return entryPointSizeMetrics } return { ...entryPointSizeMetrics, singleExportSizes: entryPointSizeMetrics.singleExportSizes.filter(hasBundleSizeChange), } }) return [ '### Size', entryPointSizeChanges.length ? getTableHtmlForSizeMetrics(entryPointSizeChanges) : 'No changes', getDetails({ summary: 'All size metrics', content: getTableHtmlForSizeMetrics(entryPointSizeMetrics), }), ].join('\n\n') } /** * @param {import('./get-package-size.mjs').EntryPointSize[]} entryPointSizesHead * @param {import('./get-package-size.mjs').EntryPointSize[]} entryPointSizesBase */ function getEntryPointSizeMetrics(entryPointSizesHead, entryPointSizesBase) { const baseEntryPointSizesMap = new Map( entryPointSizesBase.map((entryPointSize) => [ entryPointSize.bundleSize.label, entryPointSize, ]), ) return entryPointSizesHead.map(({ bundleSize, singleExportSizes }) => { const baseEntryPointSize = baseEntryPointSizesMap.get(bundleSize.label) const bundleSizeMetrics = getBundleSizeMetrics(bundleSize, baseEntryPointSize?.bundleSize) if (singleExportSizes) { const baseBundleSizeMap = new Map( baseEntryPointSize?.singleExportSizes?.map((bundleSize) => [ bundleSize.label, bundleSize, ]), ) return { bundleSize: bundleSizeMetrics, singleExportSizes: singleExportSizes.map((singleExportSize) => { const baseBundleSize = baseBundleSizeMap.get(singleExportSize.label) return getBundleSizeMetrics(singleExportSize, baseBundleSize) }), } } return { bundleSize: bundleSizeMetrics, } }) } /** * @param {import('./get-package-size.mjs').BundleSize} bundleSizeHead * @param {import('./get-package-size.mjs').BundleSize=} bundleSizeBase */ function getBundleSizeMetrics(bundleSizeHead, bundleSizeBase) { return { label: bundleSizeHead.label, size: getSizeMetrics(bundleSizeHead.size, bundleSizeBase?.size), sizeMinified: getSizeMetrics(bundleSizeHead.sizeMinified, bundleSizeBase?.sizeMinified), sizeBrotliCompressed: getSizeMetrics( bundleSizeHead.sizeBrotliCompressed, bundleSizeBase?.sizeBrotliCompressed, ), } } /** * @param {number} sizeHead * @param {number=} sizeBase */ function getSizeMetrics(sizeHead, sizeBase) { return { value: sizeHead, changePercent: sizeBase ? (sizeHead - sizeBase) / sizeBase : undefined, } } /** * * @param {ReturnType} entryPointSizeMetrics */ function getTableHtmlForSizeMetrics(entryPointSizeMetrics) { return getTableHtml({ headers: [ 'Export', 'Size original', 'Size minified', 'Size minified and Brotli compressed', ], columnAlignments: ['left', 'center', 'center', 'center'], columnWidths: ['225px', '200px', '200px', '200px'], rows: entryPointSizeMetrics.flatMap(({ bundleSize, singleExportSizes }) => { const mainBundleRow = getBundleSizeTableRow(bundleSize) if (singleExportSizes) { return [ mainBundleRow, ...singleExportSizes.map((singleExportSize) => { return getBundleSizeTableRow(singleExportSize, true) }), ] } return [mainBundleRow] }), }) } /** * @param {ReturnType} bundleSizeMetrics * @param {boolean=} isIndented */ function getBundleSizeTableRow(bundleSizeMetrics, isIndented) { return [ nonBreaking( [isIndented ? ' › ' : '', '', bundleSizeMetrics.label, ''].join(''), ), getSizeMetricsTableContent(bundleSizeMetrics.size), getSizeMetricsTableContent(bundleSizeMetrics.sizeMinified), getSizeMetricsTableContent(bundleSizeMetrics.sizeBrotliCompressed), ] } /** * @param {ReturnType} size */ function getSizeMetricsTableContent(size) { const sizeString = nonBreaking(getSizeInKb(size.value)) if (size.changePercent === undefined) { return sizeString } return sizeString + ' ' + nonBreaking(getChangePercentString(size.changePercent)) } /** * @param {number} size */ export function getSizeInKb(size) { return (size / 1024).toLocaleString('en-GB', { style: 'unit', unit: 'kilobyte', unitDisplay: 'short', minimumFractionDigits: 2, maximumFractionDigits: 2, }) } /** * @param {number} changePercent */ function getChangePercentString(changePercent) { const isZero = changePercent === 0 const isPositive = changePercent > 0 const percentageString = changePercent.toLocaleString('en-GB', { style: 'percent', minimumFractionDigits: isZero ? 0 : 1, maximumFractionDigits: 1, signDisplay: 'exceptZero', }) return percentageString + (isZero ? '' : isPositive ? ' 🔴' : ' 🟢') } /** * @param {ReturnType} bundleSizeMetrics */ function hasBundleSizeChange(bundleSizeMetrics) { return ( bundleSizeMetrics.size.changePercent !== 0 || bundleSizeMetrics.sizeMinified.changePercent !== 0 || bundleSizeMetrics.sizeBrotliCompressed.changePercent !== 0 ) } dcastil-tailwind-merge-c8c57aa/.github/actions/metrics-report/src/main.mjs000066400000000000000000000052631520007772200267550ustar00rootroot00000000000000// @ts-check import * as core from '@actions/core' import { context } from '@actions/github' import { getPackageSize } from './get-package-size.mjs' import { getSizeInKb, getSizeMetricsReportContent } from './get-size-metrics-report-content.mjs' import { setComment } from './set-comment.mjs' import { checkoutBranch } from './utils/git.mjs' run() async function run() { const pullRequest = /** @type {import('@octokit/webhooks-types').PullRequestEvent} */ ( context.payload ).pull_request if (!pullRequest) { throw Error('This action can only be run in a pull request') } core.info('Getting local package sizes') const entryPointSizesHead = await getPackageSize() logEntryPointSizes(entryPointSizesHead) await checkoutBranch(pullRequest.base.ref) core.info('Getting PR base package sizes') const entryPointSizesBase = await getPackageSize({ shouldOmitFailures: true }) logEntryPointSizes(entryPointSizesBase) const commentBody = getBodyText([ ['### Metrics report'], [ `At head commit ${pullRequest.head?.sha} and base commit ${pullRequest.base?.sha} at \`${new Date().toISOString()}\``, ], [getSizeMetricsReportContent(entryPointSizesHead, entryPointSizesBase)], ]) const isPullRequestFromFork = pullRequest.head.repo?.full_name !== `${context.repo.owner}/${context.repo.repo}` if (isPullRequestFromFork) { core.info('Pull request is from a fork, printing comment instead of posting it') core.info(commentBody) } else { await setComment(commentBody) } } /** * @param {import('./get-package-size.mjs').EntryPointSize[]} entryPointSizes */ function logEntryPointSizes(entryPointSizes) { core.info('Package sizes') entryPointSizes.forEach(({ bundleSize, singleExportSizes }) => { logBundleSize(bundleSize) singleExportSizes?.forEach((singleExportSize) => { logBundleSize(singleExportSize, true) }) }) } /** * @param {import('./get-package-size.mjs').BundleSize} bundleSize * @param {boolean=} isIndented */ function logBundleSize(bundleSize, isIndented) { core.info( [ [isIndented ? ' ' : '', bundleSize.label].join('').padEnd(30), [ getSizeInKb(bundleSize.size).padStart(14), `${getSizeInKb(bundleSize.sizeMinified).padStart(14)} minified`, `${getSizeInKb(bundleSize.sizeBrotliCompressed).padStart(14)} brotli compressed`, ].join(' '), ].join(''), ) } /** * @param {string[][]} paragraphs */ function getBodyText(paragraphs) { return paragraphs.map((lines) => lines.join('\n')).join('\n\n') } dcastil-tailwind-merge-c8c57aa/.github/actions/metrics-report/src/set-comment.mjs000066400000000000000000000050771520007772200302670ustar00rootroot00000000000000// @ts-check import * as core from '@actions/core' import { context } from '@actions/github' import { octokit } from './utils/github.mjs' const commentIdComment = '' const commentAdditionComment = "\n\n\n" /** * @param {string} body */ export async function setComment(body) { const commentToUpdate = await findCommentToUpdate() if (commentToUpdate) { await updateComment(body, commentToUpdate.id) } else { await createComment(body) } } async function findCommentToUpdate() { if (!context.payload.pull_request) { throw new Error('Can only list comments in a pull request') } core.info('Searching comment to update') const iterator = octokit.paginate.iterator(octokit.rest.issues.listComments, { owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.pull_request.number, }) /** @type {import('@octokit/types').GetResponseDataTypeFromEndpointMethod[number]=} */ let commentToUpdate iteratorLoop: for await (const response of iterator) { for (const comment of response.data) { if (comment.body?.startsWith(commentIdComment)) { commentToUpdate = comment break iteratorLoop } } } if (commentToUpdate) { core.info(`Found comment to update with URL ${commentToUpdate.url}`) } else { core.info('No comment to update found') } return commentToUpdate } /** * @param {string} body */ async function createComment(body) { if (!context.payload.pull_request) { throw new Error('Can only create a comment in a pull request') } const response = await octokit.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.pull_request.number, body: commentIdComment + commentAdditionComment + body, }) core.info(`Created comment with URL ${response.data.url}`) } /** * @param {string} body * @param {number} commentId */ async function updateComment(body, commentId) { const response = await octokit.rest.issues.updateComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: commentId, body: commentIdComment + commentAdditionComment + body, }) core.info(`Updated comment with URL ${response.data.url}`) } dcastil-tailwind-merge-c8c57aa/.github/actions/metrics-report/src/utils/000077500000000000000000000000001520007772200264505ustar00rootroot00000000000000dcastil-tailwind-merge-c8c57aa/.github/actions/metrics-report/src/utils/git.mjs000066400000000000000000000007511520007772200277510ustar00rootroot00000000000000// @ts-check import * as core from '@actions/core' import { exec } from '@actions/exec' /** * @param {string} branch */ export async function checkoutBranch(branch) { try { core.info(`Fetching branch ${branch}`) await exec(`git fetch origin ${branch} --depth=1`) } catch (error) { core.error('git fetch failed', error.message) throw error } core.info(`Checking out branch ${branch}`) await exec(`git checkout --force ${branch}`) } dcastil-tailwind-merge-c8c57aa/.github/actions/metrics-report/src/utils/github.mjs000066400000000000000000000003031520007772200304410ustar00rootroot00000000000000// @ts-check import * as core from '@actions/core' import { getOctokit } from '@actions/github' const githubToken = core.getInput('github_token') export const octokit = getOctokit(githubToken) dcastil-tailwind-merge-c8c57aa/.github/actions/metrics-report/src/utils/html.mjs000066400000000000000000000071261520007772200301350ustar00rootroot00000000000000// @ts-check /** * @typedef {object} DetailsData * @property {string} summary * @property {string} content */ /** * @param {DetailsData} param0 */ export function getDetails({ summary, content }) { return [`
`, `${summary}`, '', content, '', `
`].join('\n') } /** * @typedef {object} TableData * @property {('left' | 'center' | 'right' | undefined)[]=} columnAlignments * @property {(string | undefined)[]=} columnWidths * @property {string[]} headers * @property {string[][]} rows */ /** * @param {TableData} param0 */ export function getTableHtml({ columnAlignments = [], columnWidths = [], headers, rows }) { return getHtml([ { tag: 'table', children: [ { tag: 'thead', children: [ { tag: 'tr', children: headers.map((header, headerIndex) => ({ tag: 'th', attributes: { width: columnWidths[headerIndex], align: columnAlignments[headerIndex], }, children: [header], })), }, ], }, { tag: 'tbody', children: rows.map((row) => ({ tag: 'tr', children: row.map((cell, cellIndex) => ({ tag: 'td', attributes: { align: columnAlignments[cellIndex], }, children: [cell], })), })), }, ], }, ]) } /** * @typedef {object} HtmlElement * @property {string} tag * @property {Record=} attributes * @property {(HtmlElement | string)[]=} children */ /** * @param {(HtmlElement | string)[]} elements */ function getHtml(elements) { return indent(getHtmlLinesToIndent(elements)) } /** * @typedef {(string | LinesToIndent)[]} LinesToIndent */ /** * @param {LinesToIndent} lines * @param {number=} level * @returns {string} */ function indent(lines, level = 0) { const indentation = ' '.repeat(level * 4) return lines .map((element) => { if (typeof element === 'string') { return indentation + element } return indent(element, level + 1) }) .join('\n') } /** * @param {(HtmlElement | string)[]} elements * @returns {LinesToIndent} */ function getHtmlLinesToIndent(elements) { return elements.flatMap((element) => { if (typeof element === 'string') { return element } const attributes = Object.entries(element.attributes ?? {}) .filter(([, value]) => value !== undefined) .map(([key, value]) => `${key}="${value}"`) .join(' ') const openingTag = `<${element.tag}${attributes ? ' ' + attributes : ''}>` const closingTag = `` const innerHtmlLines = element.children ? getHtmlLinesToIndent(element.children) : [] if (innerHtmlLines.length === 0) { return [openingTag, closingTag] } return [openingTag, innerHtmlLines, closingTag] }) } /** * @param {string} text */ export function nonBreaking(text) { return text.replace(/ /g, ' ') } dcastil-tailwind-merge-c8c57aa/.github/actions/metrics-report/src/utils/path.mjs000066400000000000000000000004311520007772200301150ustar00rootroot00000000000000// @ts-check import path from 'path' import { fileURLToPath } from 'url' const currentDirPath = path.dirname(fileURLToPath(import.meta.url)) export const actionRootPath = path.resolve(currentDirPath, '../..') export const repoRootPath = path.resolve(actionRootPath, '../../..') dcastil-tailwind-merge-c8c57aa/.github/actions/metrics-report/yarn.lock000066400000000000000000000635121520007772200263530ustar00rootroot00000000000000# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. # yarn lockfile v1 "@actions/core@^3.0.1": version "3.0.1" resolved "https://registry.yarnpkg.com/@actions/core/-/core-3.0.1.tgz#0f4d8b14527ee51e0db061eedc24a206c9f98c23" integrity sha512-a6d/Nwahm9fliVGRhdhofo40HjHQasUPusmc7vBfyky+7Z+P2A1J68zyFVaNcEclc/Se+eO595oAr5nwEIoIUA== dependencies: "@actions/exec" "^3.0.0" "@actions/http-client" "^4.0.0" "@actions/exec@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@actions/exec/-/exec-3.0.0.tgz#8c3464d20f0aa4068707757021d7e3c01a7ee203" integrity sha512-6xH/puSoNBXb72VPlZVm7vQ+svQpFyA96qdDBvhB8eNZOE8LtPf9L4oAsfzK/crCL8YZ+19fKYVnM63Sl+Xzlw== dependencies: "@actions/io" "^3.0.2" "@actions/github@^9.1.1": version "9.1.1" resolved "https://registry.yarnpkg.com/@actions/github/-/github-9.1.1.tgz#b93be4c36a46cc67cc94d6dfd33c1e33b9c59a6f" integrity sha512-tL5JbYOBZHc0ngEnCsaDcryUizIUIlQyIMwy1Wkx93H5HzbBJ7TbiPx2PnFjBwZW0Vh05JmfFZhecE6gglYegA== dependencies: "@actions/http-client" "^3.0.2" "@octokit/core" "^7.0.6" "@octokit/plugin-paginate-rest" "^14.0.0" "@octokit/plugin-rest-endpoint-methods" "^17.0.0" "@octokit/request" "^10.0.7" "@octokit/request-error" "^7.1.0" undici "^6.23.0" "@actions/http-client@^3.0.2": version "3.0.2" resolved "https://registry.yarnpkg.com/@actions/http-client/-/http-client-3.0.2.tgz#3db9c83af9d29d51ac8c30b45bc17f7014beb1b2" integrity sha512-JP38FYYpyqvUsz+Igqlc/JG6YO9PaKuvqjM3iGvaLqFnJ7TFmcLyy2IDrY0bI0qCQug8E9K+elv5ZNfw62ZJzA== dependencies: tunnel "^0.0.6" undici "^6.23.0" "@actions/http-client@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@actions/http-client/-/http-client-4.0.0.tgz#f9754133c22802466482bf96321d42f2dba1fc82" integrity sha512-QuwPsgVMsD6qaPD57GLZi9sqzAZCtiJT8kVBCDpLtxhL5MydQ4gS+DrejtZZPdIYyB1e95uCK9Luyds7ybHI3g== dependencies: tunnel "^0.0.6" undici "^6.23.0" "@actions/io@^3.0.2": version "3.0.2" resolved "https://registry.yarnpkg.com/@actions/io/-/io-3.0.2.tgz#6f89b27a159d109836d983efa283997c23b92284" integrity sha512-nRBchcMM+QK1pdjO7/idu86rbJI5YHUKCvKs0KxnSYbVe3F51UfGxuZX4Qy/fWlp6l7gWFwIkrOzN+oUK03kfw== "@esbuild/aix-ppc64@0.28.0": version "0.28.0" resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz#7a289c158e29cbf59ea0afc83cc80f06d1c89402" integrity sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA== "@esbuild/android-arm64@0.28.0": version "0.28.0" resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz#b8828d9edfa3a92660644eb8de6e4f3c203d7b17" integrity sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw== "@esbuild/android-arm@0.28.0": version "0.28.0" resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.28.0.tgz#5ec1847605e05b5dbe5df90db9ff7e3e4c58dca7" integrity sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ== "@esbuild/android-x64@0.28.0": version "0.28.0" resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.28.0.tgz#390642175b88ef82bad4cce03f8ab13fe9b1912e" integrity sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA== "@esbuild/darwin-arm64@0.28.0": version "0.28.0" resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz#ae45325960d5950cd6951e4f97396f4e1ff7d8d3" integrity sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q== "@esbuild/darwin-x64@0.28.0": version "0.28.0" resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz#c079247d589b6b99449659d94f06951b84bff2e4" integrity sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ== "@esbuild/freebsd-arm64@0.28.0": version "0.28.0" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz#45c456215a486593c94900297202dc11c880a37a" integrity sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q== "@esbuild/freebsd-x64@0.28.0": version "0.28.0" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz#0399494c1c85e4388e9b7040bd60d48f2a5b0d2c" integrity sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw== "@esbuild/linux-arm64@0.28.0": version "0.28.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz#d6d9f09ef0de54116bf459a4d53cac7e0952fe39" integrity sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A== "@esbuild/linux-arm@0.28.0": version "0.28.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz#7b42ffa84c288ae94fdc431c1b28a89e3c3b9278" integrity sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw== "@esbuild/linux-ia32@0.28.0": version "0.28.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz#deb15d112ed8dd605346b6b953d23a21ff81253f" integrity sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ== "@esbuild/linux-loong64@0.28.0": version "0.28.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz#81fb89d07eecc79b157dea61033757726fce0ca4" integrity sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg== "@esbuild/linux-mips64el@0.28.0": version "0.28.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz#d0e42691b3ff7af9fb2217b70fc01f343bdb62bb" integrity sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w== "@esbuild/linux-ppc64@0.28.0": version "0.28.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz#389f3e5e98f17d477c467cc87136e1a076eead87" integrity sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg== "@esbuild/linux-riscv64@0.28.0": version "0.28.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz#763bd60d59b242be12da1e67d5729f3024c605fa" integrity sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ== "@esbuild/linux-s390x@0.28.0": version "0.28.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz#aac6061634872e4677de693bce8030d73b1fd055" integrity sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q== "@esbuild/linux-x64@0.28.0": version "0.28.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz#4f2917747188fe77632bcec65b2d84b422419779" integrity sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ== "@esbuild/netbsd-arm64@0.28.0": version "0.28.0" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz#814df0ae57a0c386814491b8397eeba82094a947" integrity sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw== "@esbuild/netbsd-x64@0.28.0": version "0.28.0" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz#e01bdf7e60fa1a08e46d46d960b0d9bb8ac210af" integrity sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw== "@esbuild/openbsd-arm64@0.28.0": version "0.28.0" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz#4a15c36aacca68d2d5a4c90b710c06759f4c1ffa" integrity sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g== "@esbuild/openbsd-x64@0.28.0": version "0.28.0" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz#475e6101498a8ecce3008d7c388111d7a27c17bd" integrity sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA== "@esbuild/openharmony-arm64@0.28.0": version "0.28.0" resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz#cfdc3957f0b7a69f1bde129aad17fcc2f6fa033e" integrity sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w== "@esbuild/sunos-x64@0.28.0": version "0.28.0" resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz#a013c856fecacd1c3aec985c8afe1d1cb017497d" integrity sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw== "@esbuild/win32-arm64@0.28.0": version "0.28.0" resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz#eae05e0f35271cad3898b43168d3e9a3bbaf47e5" integrity sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA== "@esbuild/win32-ia32@0.28.0": version "0.28.0" resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz#06161ebc5bf75c08d69feb3c6b22560515913998" integrity sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA== "@esbuild/win32-x64@0.28.0": version "0.28.0" resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz#04d90d5752b4ce65d2b6ac25eba08ff7624fe07c" integrity sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw== "@octokit/auth-token@^6.0.0": version "6.0.0" resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-6.0.0.tgz#b02e9c08a2d8937df09a2a981f226ad219174c53" integrity sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w== "@octokit/core@^7.0.6": version "7.0.6" resolved "https://registry.yarnpkg.com/@octokit/core/-/core-7.0.6.tgz#0d58704391c6b681dec1117240ea4d2a98ac3916" integrity sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q== dependencies: "@octokit/auth-token" "^6.0.0" "@octokit/graphql" "^9.0.3" "@octokit/request" "^10.0.6" "@octokit/request-error" "^7.0.2" "@octokit/types" "^16.0.0" before-after-hook "^4.0.0" universal-user-agent "^7.0.0" "@octokit/endpoint@^11.0.2": version "11.0.2" resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-11.0.2.tgz#a8d955e053a244938b81d86cd73efd2dcb5ef5af" integrity sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ== dependencies: "@octokit/types" "^16.0.0" universal-user-agent "^7.0.2" "@octokit/graphql@^9.0.3": version "9.0.3" resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-9.0.3.tgz#5b8341c225909e924b466705c13477face869456" integrity sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA== dependencies: "@octokit/request" "^10.0.6" "@octokit/types" "^16.0.0" universal-user-agent "^7.0.0" "@octokit/openapi-types@^27.0.0": version "27.0.0" resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-27.0.0.tgz#374ea53781965fd02a9d36cacb97e152cefff12d" integrity sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA== "@octokit/plugin-paginate-rest@^14.0.0": version "14.0.0" resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-14.0.0.tgz#44dc9fff2dacb148d4c5c788b573ddc044503026" integrity sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw== dependencies: "@octokit/types" "^16.0.0" "@octokit/plugin-rest-endpoint-methods@^17.0.0": version "17.0.0" resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-17.0.0.tgz#8c54397d3a4060356a1c8a974191ebf945924105" integrity sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw== dependencies: "@octokit/types" "^16.0.0" "@octokit/request-error@^7.0.2", "@octokit/request-error@^7.1.0": version "7.1.0" resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-7.1.0.tgz#440fa3cae310466889778f5a222b47a580743638" integrity sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw== dependencies: "@octokit/types" "^16.0.0" "@octokit/request@^10.0.6", "@octokit/request@^10.0.7": version "10.0.7" resolved "https://registry.yarnpkg.com/@octokit/request/-/request-10.0.7.tgz#93f619914c523750a85e7888de983e1009eb03f6" integrity sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA== dependencies: "@octokit/endpoint" "^11.0.2" "@octokit/request-error" "^7.0.2" "@octokit/types" "^16.0.0" fast-content-type-parse "^3.0.0" universal-user-agent "^7.0.2" "@octokit/types@^16.0.0": version "16.0.0" resolved "https://registry.yarnpkg.com/@octokit/types/-/types-16.0.0.tgz#fbd7fa590c2ef22af881b1d79758bfaa234dbb7c" integrity sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg== dependencies: "@octokit/openapi-types" "^27.0.0" "@octokit/webhooks-types@^7.6.1": version "7.6.1" resolved "https://registry.yarnpkg.com/@octokit/webhooks-types/-/webhooks-types-7.6.1.tgz#bc96371057c2d54c982c9f8f642655b26cd588eb" integrity sha512-S8u2cJzklBC0FgTwWVLaM8tMrDuDMVE4xiTK4EYXM9GntyvrdbSoxqDQa+Fh57CCNApyIpyeqPhhFEmHPfrXgw== "@rollup/rollup-android-arm-eabi@4.60.2": version "4.60.2" resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz#a19c645c375158cd5c50a344106f0fa18eb821c4" integrity sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw== "@rollup/rollup-android-arm64@4.60.2": version "4.60.2" resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz#1af19aa9d3ad6d00df2681f59cfcb8bf7499576b" integrity sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg== "@rollup/rollup-darwin-arm64@4.60.2": version "4.60.2" resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz#3b8463e03ba2a393453fea70e7d907379c27b649" integrity sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA== "@rollup/rollup-darwin-x64@4.60.2": version "4.60.2" resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz#28da23d69fe117f5f0ff330a8549e51bd09f1b6a" integrity sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g== "@rollup/rollup-freebsd-arm64@4.60.2": version "4.60.2" resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz#94bacac3190f621de1355922b599f3817786044c" integrity sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw== "@rollup/rollup-freebsd-x64@4.60.2": version "4.60.2" resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz#8a0094f533b9fda160b5c90ad9e0c78fca341788" integrity sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ== "@rollup/rollup-linux-arm-gnueabihf@4.60.2": version "4.60.2" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz#3b7e901a555c7245c87f7440979bee0a1ec882bb" integrity sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg== "@rollup/rollup-linux-arm-musleabihf@4.60.2": version "4.60.2" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz#ee9a09b72e8ad764cfd6188b32ff1de528ff7ebe" integrity sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw== "@rollup/rollup-linux-arm64-gnu@4.60.2": version "4.60.2" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz#ba483f4aca9be141171d086dbd01ada6ab03b58d" integrity sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg== "@rollup/rollup-linux-arm64-musl@4.60.2": version "4.60.2" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz#17b595b790e6df68e91c5d02526fc832a985ce4f" integrity sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA== "@rollup/rollup-linux-loong64-gnu@4.60.2": version "4.60.2" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz#551718714075a2bfb36a2813c466e3a0e9d56abf" integrity sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A== "@rollup/rollup-linux-loong64-musl@4.60.2": version "4.60.2" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz#ba156ed1243447a3d710972001d5dcfe3827ff3d" integrity sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q== "@rollup/rollup-linux-ppc64-gnu@4.60.2": version "4.60.2" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz#6a957a709b86ac62ef68e597ac03dbd4336782b1" integrity sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw== "@rollup/rollup-linux-ppc64-musl@4.60.2": version "4.60.2" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz#ca4176b4ad53f3edee3b4bfa6f9ef48ff38f167b" integrity sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ== "@rollup/rollup-linux-riscv64-gnu@4.60.2": version "4.60.2" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz#4e6b08f72ebeafdb41f3ec433bd228ba8573473b" integrity sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A== "@rollup/rollup-linux-riscv64-musl@4.60.2": version "4.60.2" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz#a0b8b8580c7680c8086cb3226527e5472253b895" integrity sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ== "@rollup/rollup-linux-s390x-gnu@4.60.2": version "4.60.2" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz#79fe15b92ce0bae2b609cf26dd158cd3e2b73634" integrity sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA== "@rollup/rollup-linux-x64-gnu@4.60.2": version "4.60.2" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz#6aa8302fa45fd3cbbc510ccd223c9c37bf67e53f" integrity sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ== "@rollup/rollup-linux-x64-musl@4.60.2": version "4.60.2" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz#0c1a5e9799f80c47a66f2c3a5f1a280f38356047" integrity sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw== "@rollup/rollup-openbsd-x64@4.60.2": version "4.60.2" resolved "https://registry.yarnpkg.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz#5f07c863e74fd428794f1dc5749f321b661d1f17" integrity sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg== "@rollup/rollup-openharmony-arm64@4.60.2": version "4.60.2" resolved "https://registry.yarnpkg.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz#8e0d71324be0f423428b12b25a2eb8ea8e0a7833" integrity sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q== "@rollup/rollup-win32-arm64-msvc@4.60.2": version "4.60.2" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz#a553fdf90a785ace6d7501eed6241c468b088999" integrity sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ== "@rollup/rollup-win32-ia32-msvc@4.60.2": version "4.60.2" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz#0fb04f0a88027fbfd323e25a446debce4773868c" integrity sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg== "@rollup/rollup-win32-x64-gnu@4.60.2": version "4.60.2" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz#aaa9e36dbdc0f0e397e5966dcce1b4285354ede2" integrity sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA== "@rollup/rollup-win32-x64-msvc@4.60.2": version "4.60.2" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz#3418dcf1388f2abd6b0ccc08fe1ef205ae76d696" integrity sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA== "@types/estree@1.0.8": version "1.0.8" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e" integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== before-after-hook@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-4.0.0.tgz#cf1447ab9160df6a40f3621da64d6ffc36050cb9" integrity sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ== esbuild@^0.28.0: version "0.28.0" resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.28.0.tgz#5dee347ffb3e3874212a35a69836b077b1ce6d96" integrity sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw== optionalDependencies: "@esbuild/aix-ppc64" "0.28.0" "@esbuild/android-arm" "0.28.0" "@esbuild/android-arm64" "0.28.0" "@esbuild/android-x64" "0.28.0" "@esbuild/darwin-arm64" "0.28.0" "@esbuild/darwin-x64" "0.28.0" "@esbuild/freebsd-arm64" "0.28.0" "@esbuild/freebsd-x64" "0.28.0" "@esbuild/linux-arm" "0.28.0" "@esbuild/linux-arm64" "0.28.0" "@esbuild/linux-ia32" "0.28.0" "@esbuild/linux-loong64" "0.28.0" "@esbuild/linux-mips64el" "0.28.0" "@esbuild/linux-ppc64" "0.28.0" "@esbuild/linux-riscv64" "0.28.0" "@esbuild/linux-s390x" "0.28.0" "@esbuild/linux-x64" "0.28.0" "@esbuild/netbsd-arm64" "0.28.0" "@esbuild/netbsd-x64" "0.28.0" "@esbuild/openbsd-arm64" "0.28.0" "@esbuild/openbsd-x64" "0.28.0" "@esbuild/openharmony-arm64" "0.28.0" "@esbuild/sunos-x64" "0.28.0" "@esbuild/win32-arm64" "0.28.0" "@esbuild/win32-ia32" "0.28.0" "@esbuild/win32-x64" "0.28.0" fast-content-type-parse@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz#5590b6c807cc598be125e6740a9fde589d2b7afb" integrity sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg== fsevents@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== rollup@^4.60.2: version "4.60.2" resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.60.2.tgz#ac23fe4bd530304cef9fa61e639d7098b6762cf4" integrity sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ== dependencies: "@types/estree" "1.0.8" optionalDependencies: "@rollup/rollup-android-arm-eabi" "4.60.2" "@rollup/rollup-android-arm64" "4.60.2" "@rollup/rollup-darwin-arm64" "4.60.2" "@rollup/rollup-darwin-x64" "4.60.2" "@rollup/rollup-freebsd-arm64" "4.60.2" "@rollup/rollup-freebsd-x64" "4.60.2" "@rollup/rollup-linux-arm-gnueabihf" "4.60.2" "@rollup/rollup-linux-arm-musleabihf" "4.60.2" "@rollup/rollup-linux-arm64-gnu" "4.60.2" "@rollup/rollup-linux-arm64-musl" "4.60.2" "@rollup/rollup-linux-loong64-gnu" "4.60.2" "@rollup/rollup-linux-loong64-musl" "4.60.2" "@rollup/rollup-linux-ppc64-gnu" "4.60.2" "@rollup/rollup-linux-ppc64-musl" "4.60.2" "@rollup/rollup-linux-riscv64-gnu" "4.60.2" "@rollup/rollup-linux-riscv64-musl" "4.60.2" "@rollup/rollup-linux-s390x-gnu" "4.60.2" "@rollup/rollup-linux-x64-gnu" "4.60.2" "@rollup/rollup-linux-x64-musl" "4.60.2" "@rollup/rollup-openbsd-x64" "4.60.2" "@rollup/rollup-openharmony-arm64" "4.60.2" "@rollup/rollup-win32-arm64-msvc" "4.60.2" "@rollup/rollup-win32-ia32-msvc" "4.60.2" "@rollup/rollup-win32-x64-gnu" "4.60.2" "@rollup/rollup-win32-x64-msvc" "4.60.2" fsevents "~2.3.2" tunnel@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c" integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== undici@^6.23.0: version "6.24.1" resolved "https://registry.yarnpkg.com/undici/-/undici-6.24.1.tgz#9df1425cede20b836d95634347946f79578b7e71" integrity sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA== universal-user-agent@^7.0.0, universal-user-agent@^7.0.2: version "7.0.3" resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-7.0.3.tgz#c05870a58125a2dc00431f2df815a77fe69736be" integrity sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A== dcastil-tailwind-merge-c8c57aa/.github/actions/release-commenter/000077500000000000000000000000001520007772200251515ustar00rootroot00000000000000dcastil-tailwind-merge-c8c57aa/.github/actions/release-commenter/action.yml000066400000000000000000000023201520007772200271460ustar00rootroot00000000000000name: 'release-commenter' author: 'Dany Castillo' description: 'Comments on released PRs/issues with semver-aware range selection' inputs: github-token: description: 'GitHub token with issues and pull-request write permissions' default: '${{ github.token }}' required: false comment-template: description: 'Comment body template' required: false default: 'This was addressed in release {release_link}.' label-template: description: 'Comma-separated labels to add' required: false skip-label: description: 'Comma-separated labels that skip associated PR processing' required: false default: 'dependencies' base-tag: description: 'Optional manual base tag for compare range' required: false head-tag: description: 'Optional manual head tag for compare range' required: false dry-run: description: 'When true, log planned changes without posting' required: false default: 'false' npm-package-name: description: 'Optional npm package name for SHA-based prerelease base resolution' required: false runs: using: 'node20' main: 'index.js' dcastil-tailwind-merge-c8c57aa/.github/actions/release-commenter/index.js000066400000000000000000001170701520007772200266240ustar00rootroot00000000000000// @ts-check const fs = require('node:fs') const closesMatcher = /aria-label="This (?:commit|pull request) closes issue #(\d+)\."/g const releaseTagMarkerRegex = //i const releaseTagUrlRegex = /https:\/\/github\.com\/[^/\s]+\/[^/\s]+\/releases\/tag\/([^)\s]+)/i const defaultCommentTemplate = 'This was addressed in release {release_link}.' const apiVersionHeader = '2022-11-28' const npmRegistryUrl = process.env.NPM_REGISTRY_URL || 'https://registry.npmjs.org' const eventPath = process.env.GITHUB_EVENT_PATH const repository = process.env.GITHUB_REPOSITORY const apiUrl = process.env.GITHUB_API_URL || 'https://api.github.com' const graphqlUrl = process.env.GITHUB_GRAPHQL_URL || `${apiUrl}/graphql` const githubServerUrl = process.env.GITHUB_SERVER_URL || 'https://github.com' const githubRunId = process.env.GITHUB_RUN_ID || '' main().catch((error) => { const message = error instanceof Error ? error.message : String(error) logError(message) process.exit(1) }) /** * @typedef {{ * major: number * minor: number * patch: number * prerelease: string[] * isPrerelease: boolean * }} ParsedTag */ /** * @typedef {{ * tag_name: string * name?: string | null * html_url?: string | null * draft?: boolean * }} ReleaseRecord */ /** * @typedef {{ * tag: string * name: string * htmlUrl: string * }} ReleaseInfo */ /** * @typedef {{ * release?: ReleaseRecord * }} EventPayload */ /** * @typedef {{ * sha: string * }} CommitRecord */ /** * @typedef {{ * status: string * ahead_by?: number * commits: CommitRecord[] * }} CompareData */ /** * @typedef {{ * body?: string | null * html_url: string * }} IssueComment */ /** * @typedef {{ * issueNumber: number * kind: 'duplicate' | 'previous-release' * existingTag: string * commentUrl: string * }} GuardViolation */ /** * @typedef {{ * name: string * }} GraphQLLabelNode */ /** * @typedef {{ * __typename: 'Issue' | 'PullRequest' * number: number * }} GraphQLTimelineSubject */ /** * @typedef {{ * __typename: 'ConnectedEvent' | 'DisconnectedEvent' * isCrossRepository: boolean * subject?: GraphQLTimelineSubject | null * }} GraphQLTimelineNode */ /** * @typedef {{ * bodyHTML?: string * number: number * labels?: { nodes?: GraphQLLabelNode[] | null } | null * timelineItems?: { nodes?: GraphQLTimelineNode[] | null } | null * }} GraphQLAssociatedPrNode */ /** * @typedef {{ * node: GraphQLAssociatedPrNode * }} GraphQLAssociatedPrEdge */ /** * @typedef {{ * messageHeadlineHTML?: string * messageBodyHTML?: string * associatedPullRequests?: { edges?: GraphQLAssociatedPrEdge[] | null } | null * }} GraphQLCommitResource */ /** * @typedef {{ * resource?: GraphQLCommitResource | null * }} GraphQLCommitQueryData */ /** * @typedef {{ * prereleasePrefix: string * currentSha: string * }} ShaPrereleaseContext */ /** * @typedef {{ * versions?: Record * }} NpmPackument */ /** * @typedef {{ * baseRef: string * headRef: string * sourceVersion: string * aheadBy: number * }} ShaBaseResolution */ /** * @typedef {{ * baseRef: string * headRef: string * }} CompareRefs */ /** * Action entrypoint. * * @returns {Promise} */ async function main() { if (!repository) { throw new Error('Missing GITHUB_REPOSITORY') } const [owner, repo] = repository.split('/') if (!owner || !repo) { throw new Error(`Invalid GITHUB_REPOSITORY value: "${repository}"`) } const payload = readEventPayload() const token = getInput('github-token') || process.env.GITHUB_TOKEN if (!token) { throw new Error('Missing github-token input') } const headTagInput = getInput('head-tag') const baseTagInput = getInput('base-tag') const npmPackageNameInput = getInput('npm-package-name') const dryRun = parseBooleanInput(getInput('dry-run', 'false')) const currentTag = headTagInput || payload?.release?.tag_name if (!currentTag) { throw new Error( 'Could not determine current tag. Set head-tag input or trigger from release.published', ) } const currentVersion = parseTag(currentTag) if (!currentVersion) { throw new Error(`Current tag "${currentTag}" is not semver-compatible`) } const shaPrereleaseContext = getShaPrereleaseContext(currentVersion) log(`Current tag: ${currentTag}`) log(`Dry run: ${dryRun}`) const currentRelease = await getCurrentRelease(token, owner, repo, currentTag, payload?.release) const releaseLabel = currentRelease.name || currentRelease.tag const npmPackageName = npmPackageNameInput || repo const releaseUrl = shaPrereleaseContext ? npmVersionUrl(npmPackageName, currentTag) : currentRelease.htmlUrl if (shaPrereleaseContext) { log(`Using npm release URL for prerelease tag: ${releaseUrl}`) } const commentTemplate = getInput('comment-template', defaultCommentTemplate) const labelTemplate = getInput('label-template') const skipLabelTemplate = getInput('skip-label') const resolvedComment = commentTemplate ? resolveTemplate(commentTemplate, releaseLabel, currentRelease.tag, releaseUrl).trim() : '' const labels = parseTemplateList(labelTemplate, releaseLabel, currentRelease.tag, releaseUrl) const skipLabels = parseTemplateList( skipLabelTemplate, releaseLabel, currentRelease.tag, releaseUrl, ) const { baseRef, headRef } = await resolveCompareRefs( token, owner, repo, currentTag, currentVersion, shaPrereleaseContext, baseTagInput, npmPackageName, ) log(`Base ref: ${baseRef}`) log(`Head ref: ${headRef}`) log(`Compare range: ${baseRef}...${headRef}`) const compareData = await requestJson( token, 'GET', `/repos/${owner}/${repo}/compare/${encodeURIComponent(baseRef)}...${encodeURIComponent(headRef)}`, ) /** @type {CompareData} */ const comparePayload = compareData const commits = comparePayload.commits || [] log(`Compare status: ${comparePayload.status}`) log(`Commits in range: ${commits.length}`) if (!commits.length) { log('No commits found in compare range. Nothing to do.') return } const targets = await collectLinkedIssuesAndPrs(token, owner, repo, commits, skipLabels) log(`Resolved targets: ${targets.length}`) if (!targets.length) { log('No linked issues/PRs found. Nothing to do.') return } const guardViolations = await checkGuardViolations(token, owner, repo, targets, currentVersion) if (guardViolations.length > 0) { const details = guardViolations .slice(0, 20) .map( (violation) => `#${violation.issueNumber} (${violation.kind}, existing ${violation.existingTag}) ${violation.commentUrl}`, ) .join('\n') throw new Error( `Guard check failed. Found ${guardViolations.length} target(s) with existing release comments.\n${details}`, ) } let commentBody = '' if (resolvedComment) { commentBody = `${resolvedComment}\n\n${releaseCommentMarker( currentTag, currentVersion.isPrerelease, getCurrentRunUrl(), )}` } if (dryRun && commentBody) { log(`Dry run comment body:\n${commentBody}`) } await postCommentsAndLabels(token, owner, repo, targets, commentBody, labels, dryRun) if (dryRun) { log('Dry run complete; no comments/labels were posted.') } else { log('Completed posting release comments/labels.') } } /** * Resolves the compare refs for this run from manual input, SHA-prerelease npm strategy, * or semver-stable release history strategy. * * @param {string} token * @param {string} owner * @param {string} repo * @param {string} currentTag * @param {ParsedTag} currentVersion * @param {ShaPrereleaseContext | null} shaPrereleaseContext * @param {string} baseTagInput * @param {string} npmPackageName * @returns {Promise} */ async function resolveCompareRefs( token, owner, repo, currentTag, currentVersion, shaPrereleaseContext, baseTagInput, npmPackageName, ) { let baseRef = '' let headRef = currentTag if (baseTagInput) { baseRef = baseTagInput if (shaPrereleaseContext) { headRef = shaPrereleaseContext.currentSha } log(`Using manual base ref input: ${baseRef}`) } else if (shaPrereleaseContext) { const shaResolution = await resolveShaPrereleaseBaseRef( token, owner, repo, npmPackageName, currentVersion, shaPrereleaseContext, ) baseRef = shaResolution.baseRef headRef = shaResolution.headRef log( `Resolved SHA compare range from npm: version=${shaResolution.sourceVersion} base_sha=${baseRef} head_sha=${headRef} ahead_by=${shaResolution.aheadBy}`, ) } else { const releases = await requestPaginated(token, `/repos/${owner}/${repo}/releases`) /** @type {ReleaseRecord[]} */ const publishedReleases = releases.filter((release) => !release.draft) baseRef = pickBaseTag(currentTag, currentVersion, publishedReleases, '') } return { baseRef, headRef } } /** * Resolves linked issue/PR numbers from commits and associated PR metadata. * * @param {string} token * @param {string} owner * @param {string} repo * @param {CommitRecord[]} commits * @param {string[]} skipLabels * @returns {Promise} */ async function collectLinkedIssuesAndPrs(token, owner, repo, commits, skipLabels) { /** @type {Set} */ const linkedIssuesPrs = new Set() for (const commit of commits) { const commitUrl = `https://github.com/${owner}/${repo}/commit/${commit.sha}` const data = await requestGraphQL( token, ` query($url: URI!) { resource(url: $url) { ... on Commit { messageHeadlineHTML messageBodyHTML associatedPullRequests(first: 20) { pageInfo { hasNextPage } edges { node { bodyHTML number labels(first: 50) { pageInfo { hasNextPage } nodes { name } } timelineItems(itemTypes: [CONNECTED_EVENT, DISCONNECTED_EVENT], first: 100) { pageInfo { hasNextPage } nodes { ... on ConnectedEvent { __typename isCrossRepository subject { __typename ... on Issue { number } ... on PullRequest { number } } } ... on DisconnectedEvent { __typename isCrossRepository subject { __typename ... on Issue { number } ... on PullRequest { number } } } } } } } } } } } `, { url: commitUrl }, ) /** @type {GraphQLCommitQueryData} */ const queryData = data const resource = queryData.resource if (!resource) continue const commitHtmlSegments = [ resource.messageHeadlineHTML || '', resource.messageBodyHTML || '', ...(resource.associatedPullRequests?.edges || []).map( (edge) => edge?.node?.bodyHTML || '', ), ].join(' ') for (const match of commitHtmlSegments.matchAll(closesMatcher)) { const issueNumber = Number.parseInt(match[1], 10) if (Number.isInteger(issueNumber)) { linkedIssuesPrs.add(issueNumber) } } for (const edge of resource.associatedPullRequests?.edges || []) { const pr = edge.node const prLabels = (pr.labels?.nodes || []).map((label) => label.name) if (shouldSkipPr(skipLabels, prLabels)) { log(`Skipping PR #${pr.number} because skip-label matched`) continue } linkedIssuesPrs.add(pr.number) const timelineNodes = (pr.timelineItems?.nodes || []) .filter( (node) => !node.isCrossRepository && node.subject && (node.subject.__typename === 'Issue' || node.subject.__typename === 'PullRequest') && Number.isInteger(node.subject.number), ) .slice() .reverse() /** @type {Set} */ const seenTargets = new Set() for (const node of timelineNodes) { if (!node.subject) continue const targetNumber = node.subject.number if (seenTargets.has(targetNumber)) continue if (node.__typename === 'ConnectedEvent') { linkedIssuesPrs.add(targetNumber) } seenTargets.add(targetNumber) } } } return [...linkedIssuesPrs].sort((left, right) => left - right) } /** * Validates that targets do not already have release comments that would make this run unsafe. * * @param {string} token * @param {string} owner * @param {string} repo * @param {number[]} targetNumbers * @param {ParsedTag} currentVersion * @returns {Promise} */ async function checkGuardViolations(token, owner, repo, targetNumbers, currentVersion) { if (currentVersion.isPrerelease) { log('Current release is a prerelease, skipping duplicate/previous-release comment guard') return [] } /** @type {GuardViolation[]} */ const violations = [] for (const issueNumber of targetNumbers) { const comments = await requestPaginated( token, `/repos/${owner}/${repo}/issues/${issueNumber}/comments`, ) /** @type {IssueComment[]} */ const issueComments = comments for (const comment of issueComments) { const existingTag = extractReleaseTagFromComment(comment.body || '') if (!existingTag) continue const existingVersion = parseTag(existingTag) if (!existingVersion) continue if (compareSemver(existingVersion, currentVersion) === 0) { violations.push({ issueNumber, kind: 'duplicate', existingTag, commentUrl: comment.html_url, }) break } if (!existingVersion.isPrerelease) { violations.push({ issueNumber, kind: 'previous-release', existingTag, commentUrl: comment.html_url, }) break } } } return violations } /** * Posts comments and/or labels for all resolved targets, or logs them in dry-run mode. * * @param {string} token * @param {string} owner * @param {string} repo * @param {number[]} targets * @param {string} commentBody * @param {string[]} labels * @param {boolean} dryRun * @returns {Promise} */ async function postCommentsAndLabels(token, owner, repo, targets, commentBody, labels, dryRun) { for (const issueNumber of targets) { if (commentBody) { if (dryRun) { log(`Dry run: would comment on #${issueNumber}`) } else { await requestJson( token, 'POST', `/repos/${owner}/${repo}/issues/${issueNumber}/comments`, undefined, { body: commentBody }, ) } } if (labels.length > 0) { if (dryRun) { log(`Dry run: would add labels [${labels.join(', ')}] to #${issueNumber}`) } else { await requestJson( token, 'POST', `/repos/${owner}/${repo}/issues/${issueNumber}/labels`, undefined, { labels }, ) } } } } /** * Writes a namespaced log line for easier workflow debugging. * * @param {string} message */ function log(message) { // eslint-disable-next-line no-console -- Intentional logging console.log(`[release-commenter] ${message}`) } /** * Writes a namespaced error line for easier workflow debugging. * * @param {string} message */ function logError(message) { const normalizedMessage = message.startsWith('Error:') ? message : `Error: ${message}` // eslint-disable-next-line no-console -- Error logging for action run diagnostics console.error(`[release-commenter] ${normalizedMessage}`) } /** * Reads an action input by name and returns a normalized string value. * * @param {string} name * @param {string} [fallback] * @returns {string} */ function getInput(name, fallback = '') { // GitHub Actions exposes JS action inputs as INPUT_ where only spaces are normalized. // For compatibility with local runs and historical code, also support "-" -> "_" fallback. const canonicalKey = `INPUT_${name.replace(/ /g, '_').toUpperCase()}` const legacyKey = canonicalKey.replaceAll('-', '_') return (process.env[canonicalKey] ?? process.env[legacyKey] ?? fallback).trim() } /** * Parses typical truthy string forms used in workflow inputs. * * @param {string} rawValue * @returns {boolean} */ function parseBooleanInput(rawValue) { return ['1', 'true', 'yes', 'on'].includes(rawValue.trim().toLowerCase()) } /** * Parses a semver tag with optional `v` or `refs/tags/` prefixes. * * @param {string | null | undefined} tag * @returns {ParsedTag | null} */ function parseTag(tag) { if (!tag) return null let normalized = tag.trim() if (normalized.startsWith('refs/tags/')) { normalized = normalized.slice('refs/tags/'.length) } if (normalized.startsWith('v')) { normalized = normalized.slice(1) } const match = normalized.match( /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+([0-9A-Za-z.-]+))?$/, ) if (!match) return null const [, majorRaw, minorRaw, patchRaw, prereleaseRaw] = match const prerelease = prereleaseRaw ? prereleaseRaw.split('.') : [] return { major: Number.parseInt(majorRaw, 10), minor: Number.parseInt(minorRaw, 10), patch: Number.parseInt(patchRaw, 10), prerelease, isPrerelease: prerelease.length > 0, } } /** * Compares two prerelease identifiers according to semver precedence rules. * * @param {string} a * @param {string} b * @returns {-1 | 0 | 1} */ function comparePrereleaseIdentifiers(a, b) { const aIsNumber = /^[0-9]+$/.test(a) const bIsNumber = /^[0-9]+$/.test(b) if (aIsNumber && bIsNumber) { const aNumber = Number.parseInt(a, 10) const bNumber = Number.parseInt(b, 10) return aNumber === bNumber ? 0 : aNumber < bNumber ? -1 : 1 } if (aIsNumber && !bIsNumber) return -1 if (!aIsNumber && bIsNumber) return 1 if (a === b) return 0 return a < b ? -1 : 1 } /** * Compares two parsed semver values. * * @param {ParsedTag} a * @param {ParsedTag} b * @returns {-1 | 0 | 1} */ function compareSemver(a, b) { if (a.major !== b.major) return a.major < b.major ? -1 : 1 if (a.minor !== b.minor) return a.minor < b.minor ? -1 : 1 if (a.patch !== b.patch) return a.patch < b.patch ? -1 : 1 if (!a.isPrerelease && !b.isPrerelease) return 0 if (a.isPrerelease && !b.isPrerelease) return -1 if (!a.isPrerelease && b.isPrerelease) return 1 const maxLength = Math.max(a.prerelease.length, b.prerelease.length) for (let index = 0; index < maxLength; index += 1) { const aIdentifier = a.prerelease[index] const bIdentifier = b.prerelease[index] if (aIdentifier === undefined) return -1 if (bIdentifier === undefined) return 1 const identifierComparison = comparePrereleaseIdentifiers(aIdentifier, bIdentifier) if (identifierComparison !== 0) return identifierComparison } return 0 } /** * Normalizes commit SHA casing. * * @param {string} value * @returns {string} */ function normalizeSha(value) { return value.toLowerCase() } /** * Checks whether a prerelease identifier looks like a git SHA. * * @param {string} value * @returns {boolean} */ function isShaLike(value) { return /^[0-9a-f]{7,40}$/i.test(value) } /** * Compares only major/minor/patch semver parts. * * @param {ParsedTag} a * @param {ParsedTag} b * @returns {boolean} */ function sameCoreVersion(a, b) { return a.major === b.major && a.minor === b.minor && a.patch === b.patch } /** * Gets prerelease prefix (everything before the last prerelease segment). * * @param {ParsedTag} version * @returns {string} */ function getPrereleasePrefix(version) { return version.prerelease.slice(0, -1).join('.') } /** * Detects prerelease tags where the final identifier is a git SHA. * * @param {ParsedTag} version * @returns {ShaPrereleaseContext | null} */ function getShaPrereleaseContext(version) { if (!version.isPrerelease || version.prerelease.length < 2) return null const maybeSha = version.prerelease[version.prerelease.length - 1] if (!isShaLike(maybeSha)) return null const prereleasePrefix = getPrereleasePrefix(version) if (!prereleasePrefix) return null return { prereleasePrefix, currentSha: normalizeSha(maybeSha), } } /** * Builds release display metadata from release-event payload when tags match. * * @param {string} currentTag * @param {ReleaseRecord | undefined} payloadRelease * @returns {ReleaseInfo | null} */ function releaseLabelAndUrl(currentTag, payloadRelease) { if (payloadRelease?.tag_name === currentTag) { return { tag: payloadRelease.tag_name, name: payloadRelease.name || payloadRelease.tag_name, htmlUrl: payloadRelease.html_url || `https://github.com/${repository}/releases/tag/${currentTag}`, } } return null } /** * Expands template placeholders for release name/tag/link values. * * @param {string} rawTemplate * @param {string} releaseLabel * @param {string} releaseTag * @param {string} releaseUrl * @returns {string} */ function resolveTemplate(rawTemplate, releaseLabel, releaseTag, releaseUrl) { return rawTemplate .split('{release_link}') .join(`[${releaseLabel}](${releaseUrl})`) .split('{release_name}') .join(releaseLabel) .split('{release_tag}') .join(releaseTag) } /** * Resolves a comma-separated label list template into concrete label names. * * @param {string} rawTemplate * @param {string} releaseLabel * @param {string} releaseTag * @param {string} releaseUrl * @returns {string[]} */ function parseTemplateList(rawTemplate, releaseLabel, releaseTag, releaseUrl) { if (!rawTemplate) return [] return resolveTemplate(rawTemplate, releaseLabel, releaseTag, releaseUrl) .split(',') .map((entry) => entry.trim()) .filter(Boolean) } /** * Decodes URL-encoded tag values when possible. * * @param {string} rawTag * @returns {string} */ function decodeTag(rawTag) { try { return decodeURIComponent(rawTag) } catch { return rawTag } } /** * Extracts a previously-commented release tag from marker or release URL. * * @param {string | null | undefined} body * @returns {string | null} */ function extractReleaseTagFromComment(body) { if (!body) return null const markerMatch = body.match(releaseTagMarkerRegex) if (markerMatch) return decodeTag(markerMatch[1]) const urlMatch = body.match(releaseTagUrlRegex) if (urlMatch) return decodeTag(urlMatch[1]) return null } /** * Builds an HTML marker persisted in each release comment for guard checks. * * @param {string} tag * @param {boolean} isPrerelease * @param {string | null} runUrl * @returns {string} */ function releaseCommentMarker(tag, isPrerelease, runUrl) { const runFragment = runUrl ? `; run_url=${runUrl}` : '' return `` } /** * Builds the URL for the current workflow run when available. * * @returns {string | null} */ function getCurrentRunUrl() { if (!repository || !githubRunId) return null return `${githubServerUrl}/${repository}/actions/runs/${githubRunId}` } /** * Reads and parses the GitHub event payload file. * * @returns {EventPayload} */ function readEventPayload() { if (!eventPath) { throw new Error('Missing GITHUB_EVENT_PATH') } return JSON.parse(fs.readFileSync(eventPath, 'utf8')) } /** * Builds an API URL path under the current GitHub API base URL. * * @param {string} pathname * @returns {string} */ function githubApiPath(pathname) { return `${apiUrl}${pathname}` } /** * Builds an npm registry URL path under the current registry base URL. * * @param {string} packageName * @returns {string} */ function npmRegistryPath(packageName) { const registryRoot = npmRegistryUrl.endsWith('/') ? npmRegistryUrl.slice(0, -1) : npmRegistryUrl return `${registryRoot}/${encodeURIComponent(packageName)}` } /** * Converts a git-like tag string into an npm version string. * * @param {string} tag * @returns {string} */ function npmVersionFromTag(tag) { let normalized = tag if (normalized.startsWith('refs/tags/')) { normalized = normalized.slice('refs/tags/'.length) } if (normalized.startsWith('v')) { normalized = normalized.slice(1) } return normalized } /** * Builds the npm package version URL for a given tag. * * @param {string} packageName * @param {string} tag * @returns {string} */ function npmVersionUrl(packageName, tag) { const version = npmVersionFromTag(tag) return `https://www.npmjs.com/package/${packageName}/v/${encodeURIComponent(version)}` } /** * Executes an authenticated GitHub REST request and parses JSON responses. * * @template T * @param {string} token * @param {string} method * @param {string} pathname * @param {Record} [queryParams] * @param {unknown} [body] * @returns {Promise} */ async function requestJson(token, method, pathname, queryParams, body) { const url = new URL(githubApiPath(pathname)) if (queryParams) { for (const [key, value] of Object.entries(queryParams)) { if (value === undefined || value === null || value === '') continue url.searchParams.set(key, String(value)) } } const response = await fetch(url, { method, headers: { Accept: 'application/vnd.github+json', Authorization: `Bearer ${token}`, 'X-GitHub-Api-Version': apiVersionHeader, 'Content-Type': 'application/json', }, body: body ? JSON.stringify(body) : undefined, }) if (response.status === 204) { return null } if (!response.ok) { const errorText = await response.text() throw new Error(`${method} ${pathname} failed (${response.status}): ${errorText}`) } return /** @type {Promise} */ (response.json()) } /** * Executes a REST request and returns `null` for not-found resources. * * @template T * @param {string} token * @param {string} method * @param {string} pathname * @param {Record} [queryParams] * @param {unknown} [body] * @returns {Promise} */ async function requestJsonOrNull(token, method, pathname, queryParams, body) { try { return await requestJson(token, method, pathname, queryParams, body) } catch (error) { if (error instanceof Error && error.message.includes('failed (404):')) { return null } throw error } } /** * Fetches all pages from a REST list endpoint with `per_page=100`. * * @template T * @param {string} token * @param {string} pathname * @param {Record} [baseQueryParams] * @returns {Promise} */ async function requestPaginated(token, pathname, baseQueryParams) { /** @type {T[]} */ const records = [] for (let page = 1; ; page += 1) { const pageData = await requestJson(token, 'GET', pathname, { ...baseQueryParams, per_page: 100, page, }) if (!Array.isArray(pageData) || pageData.length === 0) break const typedPageData = /** @type {T[]} */ (pageData) records.push(...typedPageData) if (pageData.length < 100) break } return records } /** * Executes an authenticated GraphQL request. * * @template T * @param {string} token * @param {string} query * @param {Record} variables * @returns {Promise} */ async function requestGraphQL(token, query, variables) { const response = await fetch(graphqlUrl, { method: 'POST', headers: { Accept: 'application/vnd.github+json', Authorization: `Bearer ${token}`, 'X-GitHub-Api-Version': apiVersionHeader, 'Content-Type': 'application/json', }, body: JSON.stringify({ query, variables }), }) if (!response.ok) { const errorText = await response.text() throw new Error(`POST /graphql failed (${response.status}): ${errorText}`) } const payload = /** @type {{data: T, errors?: Array<{message: string}>}} */ ( await response.json() ) if (payload.errors?.length) { throw new Error( `GraphQL errors: ${payload.errors .map((graphQLError) => graphQLError.message) .join('; ')}`, ) } return payload.data } /** * Fetches all published npm versions for a package. * * @param {string} packageName * @returns {Promise} */ async function fetchNpmVersions(packageName) { const response = await fetch(npmRegistryPath(packageName), { headers: { Accept: 'application/json', }, }) if (!response.ok) { const errorText = await response.text() throw new Error( `npm registry request failed (${response.status}) for ${packageName}: ${errorText}`, ) } const packument = /** @type {NpmPackument} */ (await response.json()) return Object.keys(packument.versions || {}) } /** * Resolves a base SHA for SHA-suffixed prerelease tags by using npm-published versions * with the same prerelease prefix and choosing the nearest ancestor in git history. * * @param {string} token * @param {string} owner * @param {string} repo * @param {string} packageName * @param {ParsedTag} currentVersion * @param {ShaPrereleaseContext} currentContext * @returns {Promise} */ async function resolveShaPrereleaseBaseRef( token, owner, repo, packageName, currentVersion, currentContext, ) { const npmVersions = await fetchNpmVersions(packageName) log( `Using npm SHA prerelease strategy for ${packageName} (${currentContext.prereleasePrefix}), checking ${npmVersions.length} npm version(s)`, ) /** * @type {Array<{ * npmVersion: string * parsedVersion: ParsedTag * sha: string * }>} */ const matchingDevCandidates = [] for (const npmVersion of npmVersions) { const parsedNpmVersion = parseTag(npmVersion) if (!parsedNpmVersion) continue const npmContext = getShaPrereleaseContext(parsedNpmVersion) if (!npmContext) continue if (npmContext.prereleasePrefix !== currentContext.prereleasePrefix) continue if (npmContext.currentSha === currentContext.currentSha) continue matchingDevCandidates.push({ npmVersion, parsedVersion: parsedNpmVersion, sha: npmContext.currentSha, }) } const sameCoreCandidates = matchingDevCandidates.filter((candidate) => sameCoreVersion(candidate.parsedVersion, currentVersion), ) const fallbackCandidates = matchingDevCandidates.filter( (candidate) => compareSemver(candidate.parsedVersion, currentVersion) < 0, ) const highestFallbackVersion = fallbackCandidates.reduce( /** * @param {ParsedTag | null} highest * @param {{parsedVersion: ParsedTag}} candidate * @returns {ParsedTag | null} */ (highest, candidate) => !highest || compareSemver(candidate.parsedVersion, highest) > 0 ? candidate.parsedVersion : highest, null, ) const narrowedFallbackCandidates = highestFallbackVersion ? fallbackCandidates.filter( (candidate) => compareSemver(candidate.parsedVersion, highestFallbackVersion) === 0, ) : [] const candidatePool = sameCoreCandidates.length > 0 ? sameCoreCandidates : narrowedFallbackCandidates if (!candidatePool.length) { throw new Error( `No prior npm versions found for ${packageName} with prerelease prefix "${currentContext.prereleasePrefix}" (same core ${currentVersion.major}.${currentVersion.minor}.${currentVersion.patch} or earlier versions)`, ) } log( sameCoreCandidates.length > 0 ? `Found ${sameCoreCandidates.length} prior same-core dev version(s)` : `No same-core prior dev versions found; falling back to highest prior semver (${narrowedFallbackCandidates.length} candidate(s))`, ) /** @type {Map} */ const shaToVersion = new Map() for (const candidate of candidatePool) { if (!shaToVersion.has(candidate.sha)) { shaToVersion.set(candidate.sha, candidate.npmVersion) } } /** @type {{baseSha: string, sourceVersion: string, aheadBy: number} | null} */ let bestMatch = null for (const [candidateSha, sourceVersion] of shaToVersion.entries()) { try { const compareResponse = await requestJson( token, 'GET', `/repos/${owner}/${repo}/compare/${encodeURIComponent(candidateSha)}...${encodeURIComponent(currentContext.currentSha)}`, ) /** @type {CompareData} */ const comparePayload = /** @type {CompareData} */ (compareResponse) if (comparePayload.status !== 'ahead') continue const aheadBy = comparePayload.ahead_by ?? Number.POSITIVE_INFINITY if (!Number.isFinite(aheadBy) || aheadBy <= 0) continue if (!bestMatch || aheadBy < bestMatch.aheadBy) { bestMatch = { baseSha: candidateSha, sourceVersion, aheadBy, } } } catch (error) { const message = error instanceof Error ? error.message : String(error) log( `Skipping npm candidate ${sourceVersion} (${candidateSha}) because compare failed: ${message}`, ) } } if (!bestMatch) { throw new Error( `Could not resolve an ancestor prerelease commit for ${currentContext.currentSha} from npm-published versions`, ) } log( `Selected npm prerelease base: version=${bestMatch.sourceVersion} base_sha=${bestMatch.baseSha} head_sha=${currentContext.currentSha} ahead_by=${bestMatch.aheadBy}`, ) return { baseRef: bestMatch.baseSha, headRef: currentContext.currentSha, sourceVersion: bestMatch.sourceVersion, aheadBy: bestMatch.aheadBy, } } /** * Resolves display metadata for the current release tag. * * @param {string} token * @param {string} owner * @param {string} repo * @param {string} currentTag * @param {ReleaseRecord | undefined} payloadRelease * @returns {Promise} */ async function getCurrentRelease(token, owner, repo, currentTag, payloadRelease) { const directRelease = releaseLabelAndUrl(currentTag, payloadRelease) if (directRelease) return directRelease const apiRelease = await requestJsonOrNull( token, 'GET', `/repos/${owner}/${repo}/releases/tags/${encodeURIComponent(currentTag)}`, ) if (apiRelease) { return { tag: apiRelease.tag_name, name: apiRelease.name || apiRelease.tag_name, htmlUrl: apiRelease.html_url || `https://github.com/${owner}/${repo}/releases/tag/${currentTag}`, } } return { tag: currentTag, name: currentTag, htmlUrl: `https://github.com/${owner}/${repo}/releases/tag/${currentTag}`, } } /** * Picks the most recent semver-compatible base tag for comparison. * * @param {string} currentTag * @param {ParsedTag} currentVersion * @param {ReleaseRecord[]} allReleases * @param {string} manualBaseTag * @returns {string} */ function pickBaseTag(currentTag, currentVersion, allReleases, manualBaseTag) { if (manualBaseTag) { const parsedManualTag = parseTag(manualBaseTag) if (!parsedManualTag) { throw new Error(`Input base-tag "${manualBaseTag}" is not a valid semver tag`) } return manualBaseTag } /** @type {Array<{tag: string, version: ParsedTag}>} */ const parsedReleases = [] for (const release of allReleases) { const version = parseTag(release.tag_name) if (!version) continue parsedReleases.push({ tag: release.tag_name, version }) } const candidates = parsedReleases .filter((release) => release.tag !== currentTag) .filter((release) => compareSemver(release.version, currentVersion) < 0) .filter((release) => (currentVersion.isPrerelease ? true : !release.version.isPrerelease)) .sort((left, right) => compareSemver(right.version, left.version)) if (!candidates.length) { const mode = currentVersion.isPrerelease ? 'all semver tags' : 'stable tags' throw new Error(`Could not find previous release tag for ${currentTag} in ${mode}`) } return candidates[0].tag } /** * Returns true when an associated PR should be skipped based on labels. * * @param {string[]} skipLabels * @param {string[]} prLabels * @returns {boolean} */ function shouldSkipPr(skipLabels, prLabels) { if (!skipLabels.length) return false return skipLabels.some((skipLabel) => prLabels.includes(skipLabel)) } dcastil-tailwind-merge-c8c57aa/.github/funding.yml000066400000000000000000000000201520007772200222470ustar00rootroot00000000000000github: dcastil dcastil-tailwind-merge-c8c57aa/.github/release-drafter.yml000066400000000000000000000027741520007772200237040ustar00rootroot00000000000000name-template: 'v$RESOLVED_VERSION' tag-template: 'v$RESOLVED_VERSION' template: | $CHANGES **Full Changelog**: https://github.com/dcastil/tailwind-merge/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION category-template: '### $TITLE' change-template: '- $TITLE by @$AUTHOR in https://github.com/dcastil/tailwind-merge/pull/$NUMBER' change-title-escapes: '\<*_&' no-changes-template: 'No changes' categories: - title: '⚠️ Needs Changelog Edit' label: 'needs changelog edit' - title: 'Breaking Changes' label: 'breaking' - title: 'New Features' label: 'feature' - title: 'Bug Fixes' label: 'bug' - title: 'Documentation' label: 'documentation' - title: 'Other' label: 'other' exclude-labels: - 'skip changelog' version-resolver: major: labels: - 'breaking' minor: labels: - 'feature' patch: labels: - 'bug' - 'documentation' - 'other' default: 'patch' autolabeler: - label: 'feature' branch: - '/\bfeature\b/i' title: - '/\bfeature\b/i' - label: 'bug' branch: - '/\b(bug|bugfix|fix)\b/i' title: - '/\b(bug|bugfix|fix)\b/i' - label: 'documentation' branch: - '/\b(documentation|docs)\b/i' title: - '/\b(documentation|docs)\b/i' - label: 'other' branch: - '/^other\b/i' - label: 'breaking' branch: - '/^breaking-/i' dcastil-tailwind-merge-c8c57aa/.github/renovate.json000066400000000000000000000005651520007772200226260ustar00rootroot00000000000000{ "extends": [ "config:recommended", ":combinePatchMinorReleases", "group:allNonMajor", ":labels(dependencies,skip changelog)", ":assignee(dcastil)", ":enableVulnerabilityAlertsWithLabel(security)", "schedule:monthly" ], "timezone": "Europe/Berlin", "prHourlyLimit": 10, "rangeStrategy": "bump" } dcastil-tailwind-merge-c8c57aa/.github/workflows/000077500000000000000000000000001520007772200221375ustar00rootroot00000000000000dcastil-tailwind-merge-c8c57aa/.github/workflows/benchmark.yml000066400000000000000000000015631520007772200246210ustar00rootroot00000000000000name: Benchmark on: push: branches: - main pull_request: jobs: benchmark: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Use Node.js 24 uses: actions/setup-node@v6 with: node-version: 24.15.0 - name: Use node_modules cache uses: actions/cache@v5 with: path: node_modules key: yarn-node-24-lock-${{ hashFiles('yarn.lock') }} restore-keys: | yarn-node-24-lock- - run: yarn install --frozen-lockfile - name: Run benchmark uses: CodSpeedHQ/action@v4 with: token: ${{ secrets.CODSPEED_TOKEN }} mode: simulation run: yarn bench dcastil-tailwind-merge-c8c57aa/.github/workflows/codeql-analysis.yml000066400000000000000000000020671520007772200257570ustar00rootroot00000000000000name: 'CodeQL' on: push: branches: [main] schedule: - cron: '30 8 * * 5' jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write steps: - uses: actions/checkout@v6 - name: Use Node.js 24 uses: actions/setup-node@v6 with: node-version: 24.15.0 - name: Use node_modules cache uses: actions/cache@v5 with: path: node_modules key: yarn-node-24-lock-${{ hashFiles('yarn.lock') }} restore-keys: | yarn-node-24-lock- - name: Initialize CodeQL uses: github/codeql-action/init@v4 with: languages: javascript - run: yarn install --frozen-lockfile - run: yarn build - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v4 dcastil-tailwind-merge-c8c57aa/.github/workflows/comment-released-prs-and-issues.yml000066400000000000000000000060501520007772200307620ustar00rootroot00000000000000name: Comment released PRs and issues on: release: types: [published] workflow_run: workflows: ['npm Publish'] types: [completed] branches: [main] workflow_dispatch: inputs: head_tag: description: 'Optional head tag override' required: false type: string base_tag: description: 'Optional base tag override' required: false type: string dry_run: description: 'Run without posting comments' required: false type: boolean default: false npm_package_name: description: 'Optional npm package name override' required: false type: string jobs: release: if: >- ${{ github.event_name != 'workflow_run' || (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push' && github.event.workflow_run.head_branch == 'main') }} runs-on: ubuntu-latest permissions: contents: read issues: write pull-requests: write steps: - uses: actions/checkout@v6 - if: github.event_name == 'workflow_run' id: dev_head_tag env: GH_TOKEN: ${{ github.token }} HEAD_SHA: ${{ github.event.workflow_run.head_sha }} run: | if [[ ! "$HEAD_SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then echo "::error::Unexpected workflow_run head SHA: ${HEAD_SHA}" exit 1 fi # Read package metadata from the published main commit as inert JSON instead of checking out the triggering SHA. version="$(gh api \ -H "Accept: application/vnd.github.v3.raw+json" \ "/repos/${GITHUB_REPOSITORY}/contents/package.json?ref=${HEAD_SHA}" \ | node -p 'JSON.parse(require("node:fs").readFileSync(0, "utf8")).version')" echo "head_tag=v${version}-dev.${HEAD_SHA}" >> "$GITHUB_OUTPUT" - uses: ./.github/actions/release-commenter with: github-token: ${{ github.token }} comment-template: This was addressed in release {release_link}. skip-label: skip-release-comment head-tag: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.head_tag || github.event_name == 'workflow_run' && steps.dev_head_tag.outputs.head_tag || '' }} base-tag: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.base_tag || '' }} dry-run: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.dry_run || false }} npm-package-name: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.npm_package_name || '' }} dcastil-tailwind-merge-c8c57aa/.github/workflows/draft-release.yml000066400000000000000000000014111520007772200253750ustar00rootroot00000000000000name: Draft Release on: push: branches: [main] pull_request: types: [opened, reopened, synchronize] jobs: update_release_draft: if: github.event_name == 'push' runs-on: ubuntu-latest permissions: contents: write pull-requests: read steps: - uses: release-drafter/release-drafter@v7.3.0 with: token: ${{ github.token }} auto_label: if: github.event_name == 'pull_request' runs-on: ubuntu-latest permissions: contents: read pull-requests: write steps: - uses: release-drafter/release-drafter/autolabeler@v7.3.0 with: token: ${{ github.token }} dcastil-tailwind-merge-c8c57aa/.github/workflows/label.yml000066400000000000000000000044661520007772200237530ustar00rootroot00000000000000name: Auto Label New Issues, PRs, and Discussions on: issues: types: [opened] pull_request_target: types: [opened] discussion: types: [created] jobs: add-label: runs-on: ubuntu-latest permissions: issues: write discussions: write pull-requests: write env: GITHUB_TOKEN: ${{ github.token }} REPO_OWNER: ${{ github.repository_owner }} REPO_NAME: ${{ github.event.repository.name }} LABEL_NAME: context-v3 steps: - name: Checkout repository uses: actions/checkout@v6 - name: Add label to new issue if: github.event_name == 'issues' run: | ISSUE_NUMBER=${{ github.event.issue.number }} gh issue edit "$ISSUE_NUMBER" --add-label "$LABEL_NAME" - name: Add label to new PR if: github.event_name == 'pull_request_target' run: | PR_NUMBER=${{ github.event.pull_request.number }} gh pr edit "$PR_NUMBER" --add-label "$LABEL_NAME" - name: Get label id for discussion id: label-data if: github.event_name == 'discussion' run: | res="$(gh api \ -H "Accept: application/vnd.github+json" \ -H "X-GitHub-Api-Version: 2022-11-28" \ /repos/$REPO_OWNER/$REPO_NAME/labels/$LABEL_NAME --jq '.node_id')" echo "label_id=$res" >> $GITHUB_OUTPUT - name: Add label to new discussion uses: octokit/graphql-action@v2.x if: github.event_name == 'discussion' env: DISCUSSION_ID: ${{ github.event.discussion.node_id }} LABEL_ID: ${{ steps.label-data.outputs.label_id }} with: query: | mutation { addLabelsToLabelable( input:{ labelableId: "${{env.DISCUSSION_ID}}" labelIds: ["${{ env.LABEL_ID}}"] } ) { clientMutationId } } dcastil-tailwind-merge-c8c57aa/.github/workflows/metrics-report.yml000066400000000000000000000026531520007772200256470ustar00rootroot00000000000000name: Metrics report on: [pull_request] jobs: metrics-report: runs-on: ubuntu-latest permissions: pull-requests: write steps: - uses: actions/checkout@v6 - name: Use Node.js 24 uses: actions/setup-node@v6 with: node-version: 24.15.0 - name: Restore node_modules cache uses: actions/cache/restore@v5 with: path: node_modules key: yarn-node-24-lock-${{ hashFiles('yarn.lock') }} restore-keys: | yarn-node-24-lock- - run: yarn --frozen-lockfile - name: Save node_modules cache uses: actions/cache/save@v5 with: path: node_modules key: yarn-node-24-lock-${{ hashFiles('yarn.lock') }} - name: Use node_modules cache for metrics-report-action uses: actions/cache@v5 with: path: .github/actions/metrics-report/node_modules key: yarn-node-24-metrics-report-action-lock-${{ hashFiles('.github/actions/metrics-report/yarn.lock') }} restore-keys: | yarn-node-24-metrics-report-action-lock- - run: yarn --cwd .github/actions/metrics-report install --frozen-lockfile - uses: ./.github/actions/metrics-report dcastil-tailwind-merge-c8c57aa/.github/workflows/npm-publish.yml000066400000000000000000000062271520007772200251270ustar00rootroot00000000000000name: npm Publish on: push: branches: [main] release: types: [published] permissions: # Necessary for OIDC permissions for trusted publishing id-token: write jobs: publish-dev: if: github.event_name == 'push' && github.ref == 'refs/heads/main' runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Use Node.js 24 uses: actions/setup-node@v6 with: node-version: 24.15.0 # More info: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages#publishing-packages-to-the-npm-registry registry-url: 'https://registry.npmjs.org' - name: Use node_modules cache uses: actions/cache@v5 with: path: node_modules key: yarn-node-24-lock-${{ hashFiles('yarn.lock') }} restore-keys: | yarn-node-24-lock- - run: yarn install --frozen-lockfile - run: yarn build - uses: actions/upload-artifact@v7 with: name: build-output path: dist if-no-files-found: error - uses: martinbeentjes/npm-get-version-action@v1.3.1 id: package-version - run: yarn version --no-git-tag-version --new-version ${{ steps.package-version.outputs.current-version }}-dev.${{ github.sha }} # npm install -g npm@latest is necessary to make provenance available (available since v9.6.5 or so). More info: https://docs.npmjs.com/generating-provenance-statements - run: npm install -g npm@latest - run: npm publish --access public --tag dev publish-prod: if: github.event_name == 'release' && github.event.action == 'published' runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Use Node.js 24 uses: actions/setup-node@v6 with: node-version: 24.15.0 # More info: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages#publishing-packages-to-the-npm-registry registry-url: 'https://registry.npmjs.org' - name: Use node_modules cache uses: actions/cache@v5 with: path: node_modules key: yarn-node-24-lock-${{ hashFiles('yarn.lock') }} restore-keys: | yarn-node-24-lock- - run: yarn install --frozen-lockfile - run: yarn lint - run: yarn test - run: yarn build - uses: actions/upload-artifact@v7 with: name: build-output path: dist if-no-files-found: error - run: yarn test:exports # npm install -g npm@latest is necessary to make provenance available (available since v9.6.5 or so). More info: https://docs.npmjs.com/generating-provenance-statements - run: npm install -g npm@latest - run: npm publish dcastil-tailwind-merge-c8c57aa/.github/workflows/test.yml000066400000000000000000000017101520007772200236400ustar00rootroot00000000000000name: Test on: push: branches: ['**'] pull_request: branches: ['**'] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Use Node.js 24 uses: actions/setup-node@v6 with: node-version: 24.15.0 - name: Use node_modules cache uses: actions/cache@v5 with: path: node_modules key: yarn-node-24-lock-${{ hashFiles('yarn.lock') }} restore-keys: | yarn-node-24-lock- - run: yarn install --frozen-lockfile - run: yarn lint - run: yarn test - run: yarn build - uses: actions/upload-artifact@v7 with: name: build-output path: dist if-no-files-found: error - run: yarn test:exports dcastil-tailwind-merge-c8c57aa/.gitignore000066400000000000000000000001251520007772200205300ustar00rootroot00000000000000/.claude/ /.vscode/ /dist/ coverage node_modules/ .DS_Store *.local *.local.md .idea dcastil-tailwind-merge-c8c57aa/.prettierrc000066400000000000000000000001651520007772200207300ustar00rootroot00000000000000{ "tabWidth": 4, "semi": false, "singleQuote": true, "printWidth": 100, "trailingComma": "all" } dcastil-tailwind-merge-c8c57aa/AGENTS.md000066400000000000000000000071371520007772200200550ustar00rootroot00000000000000# AGENTS.md This repository is `tailwind-merge`, a TypeScript library that merges Tailwind class strings by removing style conflicts while preserving non-conflicting and non-Tailwind classes. ## Primary Goals - Keep merge behavior correct for supported Tailwind versions (currently v4.0-v4.2). - Keep runtime fast (browser-oriented hot path, lazy init, lightweight cache). - Keep API and type contracts stable. ## Repo Map - Source code: `src/` - Tests: `tests/` - Human docs: `docs/` - Published overview: `README.md` (autogenerated from `docs/README.md`) - Build and tooling scripts: `scripts/` - CI workflows: `.github/workflows/` - Local GitHub actions: `.github/actions/` Deeper implementation notes live in: - `.agents/tailwind-merge-internals.md` - `.agents/tailwind-css-version-update.md` (Tailwind CSS version support workflow) - `.agents/release-workflow.md` (release changelog + GitHub release notes workflow) ## Environment and Commands - Package manager: `yarn` (classic, v1 lockfile) - CI runtime: Node `24.13.0` Core commands: - `yarn lint` - `yarn test` - `yarn test:watch` - `yarn build` - `yarn test:exports` - `yarn bench` ## Working Rules for Changes 1. Treat `src/lib/default-config.ts` as the behavioral source of truth for default class groups and conflicts. 2. If behavior changes, update tests in `tests/` in the same change. 3. If public behavior/docs change, update docs in `docs/` (not directly in `README.md`). 4. `README.md` is version-generated from `docs/README.md`; do not regenerate it during normal development changes. 5. Avoid manual edits to `dist/` and `coverage/`; they are generated artifacts. 6. Validate packaging paths with `yarn build && yarn test:exports` when touching exports/build tooling. 7. For Tailwind CSS version support work, follow `.agents/tailwind-css-version-update.md`. ## Documentation Sync Policy Treat documentation updates as part of the same change, not as follow-up work. Required when relevant: 1. Update `docs/*.md` for any user-visible behavior, API, version support, or limitation changes. 2. Update `AGENTS.md` whenever agent workflow, repo conventions, required commands, or guardrails change. 3. Update `.agents/tailwind-merge-internals.md` whenever architecture, file ownership by concern, CI flow, or testing strategy changes. 4. Do not regenerate `README.md` during normal development. It is intentionally generated as part of the version/release flow so repo visitors are routed to docs for the latest release. Definition of done for every PR/change: 1. Code and tests are updated. 2. Relevant docs are updated in the same change set. 3. `AGENTS.md` and `.agents/*` guidance is reviewed and updated if the change affects how agents should work in this repo. ## Quick Validation Matrix - Parser or modifier semantics: run `tests/modifiers.test.ts`, `tests/arbitrary-variants.test.ts`, `tests/experimental-parse-class-name.test.ts`. - Class groups/conflicts/default config: run `tests/default-config.test.ts`, `tests/class-group-conflicts.test.ts`, `tests/tailwind-css-versions.test.ts`. - Public API/types: run `tests/public-api.test.ts`, `tests/type-generics.test.ts`. - Release/package surface: run `yarn build` and `yarn test:exports`. ## Release Workflow For Tailwind CSS version support work, use: - `.agents/tailwind-css-version-update.md` For release-specific steps (draft release ingestion, changelog entry generation, sponsors handling, and GitHub release text formatting), use: - `.agents/release-workflow.md` For release comments on issues/PRs: - Workflow: `.github/workflows/comment-released-prs-and-issues.yml` - Local action: `.github/actions/release-commenter/` dcastil-tailwind-merge-c8c57aa/LICENSE.md000066400000000000000000000020561520007772200201510ustar00rootroot00000000000000MIT License Copyright (c) 2021 Dany Castillo Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. dcastil-tailwind-merge-c8c57aa/README.md000066400000000000000000000037471520007772200200340ustar00rootroot00000000000000 # tailwind-merge Utility function to efficiently merge [Tailwind CSS](https://tailwindcss.com) classes in JS without style conflicts. ```ts import { twMerge } from 'tailwind-merge' twMerge('px-2 py-1 bg-red hover:bg-dark-red', 'p-3 bg-[#B91C1C]') // → 'hover:bg-dark-red p-3 bg-[#B91C1C]' ``` - Supports Tailwind v4.0 up to v4.3 (if you use Tailwind v3, use [tailwind-merge v2.6.0](https://github.com/dcastil/tailwind-merge/tree/v2.6.0)) - Works in all modern browsers and maintained Node versions - Fully typed - [Check bundle size on Bundlephobia](https://bundlephobia.com/package/tailwind-merge) ## Get started - [What is it for](https://github.com/dcastil/tailwind-merge/blob/v3.6.0/docs/what-is-it-for.md) - [When and how to use it](https://github.com/dcastil/tailwind-merge/blob/v3.6.0/docs/when-and-how-to-use-it.md) - [Features](https://github.com/dcastil/tailwind-merge/blob/v3.6.0/docs/features.md) - [Limitations](https://github.com/dcastil/tailwind-merge/blob/v3.6.0/docs/limitations.md) - [Configuration](https://github.com/dcastil/tailwind-merge/blob/v3.6.0/docs/configuration.md) - [Recipes](https://github.com/dcastil/tailwind-merge/blob/v3.6.0/docs/recipes.md) - [API reference](https://github.com/dcastil/tailwind-merge/blob/v3.6.0/docs/api-reference.md) - [Writing plugins](https://github.com/dcastil/tailwind-merge/blob/v3.6.0/docs/writing-plugins.md) - [Versioning](https://github.com/dcastil/tailwind-merge/blob/v3.6.0/docs/versioning.md) - [Contributing](https://github.com/dcastil/tailwind-merge/blob/v3.6.0/docs/contributing.md) - [Similar packages](https://github.com/dcastil/tailwind-merge/blob/v3.6.0/docs/similar-packages.md) dcastil-tailwind-merge-c8c57aa/assets/000077500000000000000000000000001520007772200200445ustar00rootroot00000000000000dcastil-tailwind-merge-c8c57aa/assets/logo.svg000066400000000000000000000046161520007772200215340ustar00rootroot00000000000000 tailwind-merge dcastil-tailwind-merge-c8c57aa/docs/000077500000000000000000000000001520007772200174725ustar00rootroot00000000000000dcastil-tailwind-merge-c8c57aa/docs/README.md000066400000000000000000000023031520007772200207470ustar00rootroot00000000000000 # tailwind-merge Utility function to efficiently merge [Tailwind CSS](https://tailwindcss.com) classes in JS without style conflicts. ```ts import { twMerge } from 'tailwind-merge' twMerge('px-2 py-1 bg-red hover:bg-dark-red', 'p-3 bg-[#B91C1C]') // → 'hover:bg-dark-red p-3 bg-[#B91C1C]' ``` - Supports Tailwind v4.0 up to v4.3 (if you use Tailwind v3, use [tailwind-merge v2.6.0](https://github.com/dcastil/tailwind-merge/tree/v2.6.0)) - Works in all modern browsers and maintained Node versions - Fully typed - [Check bundle size on Bundlephobia](https://bundlephobia.com/package/tailwind-merge) ## Get started - [What is it for](./what-is-it-for.md) - [When and how to use it](./when-and-how-to-use-it.md) - [Features](./features.md) - [Limitations](./limitations.md) - [Configuration](./configuration.md) - [Recipes](./recipes.md) - [API reference](./api-reference.md) - [Writing plugins](./writing-plugins.md) - [Versioning](./versioning.md) - [Contributing](./contributing.md) - [Similar packages](./similar-packages.md) dcastil-tailwind-merge-c8c57aa/docs/api-reference.md000066400000000000000000000564261520007772200225360ustar00rootroot00000000000000# API reference Reference to all exports of tailwind-merge. ## `twMerge` ```ts function twMerge( ...classLists: Array ): string ``` Default function to use if you're using the default Tailwind config or are close enough to the default config. Check out [basic usage](./configuration.md#basic-usage) for more info. If `twMerge` doesn't work for you, you can create your own custom merge function with [`extendTailwindMerge`](#extendtailwindmerge). ## `twJoin` ```ts function twJoin( ...classLists: Array ): string ``` Function to join className strings conditionally without resolving conflicts. ```ts twJoin( 'border border-red-500', hasBackground && 'bg-red-100', hasLargeText && 'text-lg', hasLargeSpacing && ['p-2', hasLargeText ? 'leading-8' : 'leading-7'], ) ``` It is used internally within `twMerge` and a direct subset of [`clsx`](https://www.npmjs.com/package/clsx). If you use `clsx` or [`classnames`](https://www.npmjs.com/package/classnames) to apply Tailwind classes conditionally and don't need support for object arguments, you can use `twJoin` instead, it is a little faster and will save you a few hundred bytes in bundle size. Why no object support? [Read here](https://github.com/dcastil/tailwind-merge/discussions/137#discussioncomment-3481605). ## `getDefaultConfig` ```ts function getDefaultConfig(): satisfies Config ``` Function which returns the default config used by tailwind-merge. The tailwind-merge config is different from the Tailwind config. It is optimized for small bundle size and fast runtime performance because it is expected to run in the browser. ## `fromTheme` ```ts function fromTheme< AdditionalThemeGroupIds extends string = never, DefaultThemeGroupIdsInner extends string = DefaultThemeGroupIds, >(key: NoInfer): ThemeGetter ``` Function to retrieve values from a theme scale, to be used in class groups. `fromTheme` doesn't return the values from the theme scale, but rather another function which is used by tailwind-merge internally to retrieve the theme values. tailwind-merge can differentiate the theme getter function from a validator because it has a `isThemeGetter` property set to `true`. When using TypeScript, the function only allows passing the default theme group IDs as the `key` argument. If you use custom theme group IDs, you need to pass them as the generic type argument `AdditionalThemeGroupIds`. In case you aren't using the default tailwind-merge config and use a different set of theme group IDs entirely, you can also pass them as the generic type argument `DefaultThemeGroupIdsInner`. If you want to allow any keys, you can call it as `fromTheme('anything-goes-here')`. `fromTheme` can be used like this: ```ts type AdditionalClassGroupIds = 'badge' | 'badge-color' type AdditionalThemeGroupIds = 'custom-color' extendTailwindMerge({ extend: { theme: { 'custom-color': ['primary', 'secondary'], }, classGroups: { badge: [{ badge: [fromTheme('text')] }], 'badge-color': [{ badge: [fromTheme('custom-color')] }], }, }, }) ``` ## `extendTailwindMerge` ```ts function extendTailwindMerge< AdditionalClassGroupIds extends string = never, AdditionalThemeGroupIds extends string = never, >( configExtension: ConfigExtension< DefaultClassGroupIds | AdditionalClassGroupIds, DefaultThemeGroupIds | AdditionalThemeGroupIds >, ...createConfig: ((config: GenericConfig) => GenericConfig)[] ): TailwindMerge function extendTailwindMerge< AdditionalClassGroupIds extends string = never, AdditionalThemeGroupIds extends string = never, >(...createConfig: ((config: GenericConfig) => GenericConfig)[]): TailwindMerge ``` Function to create merge function with custom config which extends the default config. Use this if you use the default Tailwind config and just modified it in some places. > [!Note] > The function `extendTailwindMerge` computes a large data structure based on the config passed to it. I recommend to call it only once and store the result in a top-level variable instead of calling it inline within another repeatedly called function. You provide it a `configExtension` object which gets [merged](#mergeconfigs) with the default config. When using TypeScript and you use custom class group IDs or theme group IDs, you need to pass them as the generic type arguments `AdditionalClassGroupIds` and `AdditionalThemeGroupIds`. This is enforced to prevent accidental use of non-existing class group IDs accidentally. If you want to allow any custom keys without explicitly defining them, you can pass as `string` to both arguments. ```ts type AdditionalClassGroupIds = 'aspect-w' | 'aspect-h' | 'aspect-reset' type AdditionalThemeGroupIds = never const twMerge = extendTailwindMerge({ // ↓ Optional cache size // Here we're disabling the cache cacheSize: 0, // ↓ Optional prefix from TaiLwind config prefix: 'tw', // ↓ Optional config overrides // Only elements from the second level onwards are overridden override: { // ↓ Theme scales to override theme: { colors: ['black', 'white', 'yellow-500'], }, // ↓ Class groups to override classGroups: { // ↓ The `shadow` key here is the class group ID // ↓ Creates group of classes which have conflicting styles // Classes here: shadow-100, shadow-200, shadow-300, shadow-400, shadow-500 shadow: [{ shadow: ['100', '200', '300', '400', '500'] }], }, // ↓ Conflicts across different groups to override conflictingClassGroups: { // ↓ ID of class group which creates a conflict with… // ↓ …classes from groups with these IDs // Here we remove the default conflict between the font-size and leading class // groups. 'font-size': [], }, // ↓ Conflicts between the postfix modifier of a group and a different class group to // override conflictingClassGroupModifiers: { // You probably won't need this, but it follows the same shape as // `conflictingClassGroups`. }, // ↓ Class group IDs which should be resolved again with their postfix modifier attached. // Overrides default value. postfixLookupClassGroups: ['container-type'], // ↓ Modifiers whose order among multiple modifiers should be preserved because their // order changes which element gets targeted. Overrides default value. orderSensitiveModifiers: ['before'], }, // ↓ Optional config extensions // Follows same shape as the `override` object. extend: { // ↓ Theme scales to extend or create theme: { spacing: ['sm', 'md', 'lg'], }, // ↓ Class groups to extend or create classGroups: { // ↓ The `animate` key here is the class group ID // ↓ Adds class animate-shimmer to existing group with ID `animate` or creates // new class group if it doesn't exist. animate: ['animate-shimmer'], // ↓ Functions can also be used to match classes // They take the class part value as argument and return a boolean defining whether // it is a match. // Here we accept all string classes starting with `aspec-w-` followed by a number. 'aspect-w': [{ 'aspect-w': [(value) => Boolean(value) && !isNaN(value)] }], 'aspect-h': [{ 'aspect-h': [(value) => Boolean(value) && !isNaN(value)] }], 'aspect-reset': ['aspect-none'], // ↓ You can also use validators exported by tailwind-merge 'prose-size': [{ prose: ['base', validators.isTshirtSize] }], }, // ↓ Conflicts across different groups to extend or create conflictingClassGroups: { // ↓ ID of class group which creates a conflict with… // ↓ …classes from groups with these IDs // In this case `twMerge('aspect-w-5 aspect-none') → 'aspect-none'` 'aspect-reset': ['aspect-w', 'aspect-h'], }, // ↓ Conflicts between the postfix modifier of a group and a different class group to // extend or create conflictingClassGroupModifiers: { // You probably won't need this, but it follows the same shape as // `conflictingClassGroups`. }, // ↓ Class group IDs which should be resolved again with their postfix modifier attached. // Extends default value. postfixLookupClassGroups: ['aspect-reset'], // ↓ Modifiers whose order among multiple modifiers should be preserved because their // order changes which element gets targeted. Extends default value. orderSensitiveModifiers: ['before'], }, }) ``` Additionally, you can pass multiple `createConfig` functions (more to that in [`createTailwindMerge`](#createtailwindmerge)) which is convenient if you want to combine your config with third-party plugins. ```ts const twMerge = extendTailwindMerge({ … }, withSomePlugin) ``` If you only use plugins, you can omit the `configExtension` object as well. ```ts const twMerge = extendTailwindMerge(withSomePlugin) ``` ## `createTailwindMerge` ```ts function createTailwindMerge( ...createConfig: [() => Config, ...Array<(config: Config) => Config>] ): TailwindMerge ``` Function to create merge function with custom config. Use this function instead of [`extendTailwindMerge`](#extendtailwindmerge) if you don't need the default config or want more control over the config. > [!Note] > The function `createTailwindMerge` computes a large data structure based on the config passed to it. I recommend to call it only once and store the result in a top-level variable instead of calling it inline within another repeatedly called function. You need to provide a function which resolves to the config tailwind-merge should use for the new merge function. You can either extend from the default config or create a new one from scratch. ```ts // ↓ Callback passed to `createTailwindMerge` is called when // `twMerge` gets called the first time. const twMerge = createTailwindMerge(() => { const defaultConfig = getDefaultConfig() return { cacheSize: 0, classGroups: { ...defaultConfig.classGroups, badge: ['badge', 'badge-pill', { 'badge-dot': ['', 'sm', 'lg'] }], 'icon-size': [{ icon: ['auto', (value) => Number(value) >= 16] }], card: ['card-sm', 'card-md', 'card-lg'], }, conflictingClassGroups: { ...defaultConfig.conflictingClassGroups, badge: ['icon-size'], }, conflictingClassGroupModifiers: { ...defaultConfig.conflictingClassGroupModifiers, card: ['icon-size'], }, postfixLookupClassGroups: defaultConfig.postfixLookupClassGroups, orderSensitiveModifiers: [...defaultConfig.orderSensitiveModifiers, 'before'], } }) ``` Same as in [`extendTailwindMerge`](#extendtailwindmerge) you can use multiple `createConfig` functions which is convenient if you want to combine your config with third-party plugins. Just keep in mind that the first `createConfig` function does not get passed any arguments, whereas the subsequent functions get each passed the config from the previous function. ```ts const twMerge = createTailwindMerge(getDefaultConfig, withSomePlugin, (config) => ({ // ↓ Config returned by `withSomePlugin` ...config, classGroups: { ...config.classGroups, mySpecialClassGroup: [{ special: ['1', '2'] }], }, })) ``` But don't merge configs like that. Use [`mergeConfigs`](#mergeconfigs) instead. ## `mergeConfigs` ```ts function mergeConfigs( baseConfig: GenericConfig, configExtension: ConfigExtension, ): GenericConfig ``` Helper function to merge multiple tailwind-merge configs. Properties with the value `undefined` are skipped. When using TypeScript, you need to pass a union of all class group IDs and theme group IDs used in `configExtension` as generic arguments to `mergeConfigs` or pass `string` to both arguments to allow any IDs. ```ts const twMerge = createTailwindMerge(getDefaultConfig, (config) => mergeConfigs<'shadow' | 'animate' | 'prose'>(config, { override: { classGroups: { // ↓ Overriding existing class group shadow: [{ shadow: ['100', '200', '300', '400', '500'] }], }, }, extend: { classGroups: { // ↓ Adding value to existing class group animate: ['animate-shimmer'], // ↓ Adding new class group prose: [{ prose: ['', validators.isTshirtSize] }], }, }, }), ) ``` ## `validators` ```ts interface Validators { isAny(value: string): boolean isAnyNonArbitrary(value: string): boolean isArbitraryFamilyName(value: string): boolean isArbitraryImage(value: string): boolean isArbitraryLength(value: string): boolean isArbitraryNumber(value: string): boolean isArbitraryPosition(value: string): boolean isArbitraryShadow(value: string): boolean isArbitrarySize(value: string): boolean isArbitraryValue(value: string): boolean isArbitraryVariable(value: string): boolean isArbitraryVariableFamilyName(value: string): boolean isArbitraryVariableImage(value: string): boolean isArbitraryVariableLength(value: string): boolean isArbitraryVariablePosition(value: string): boolean isArbitraryVariableShadow(value: string): boolean isArbitraryVariableSize(value: string): boolean isArbitraryVariableWeight(value: string): boolean isArbitraryWeight(value: string): boolean isFraction(value: string): boolean isInteger(value: string): boolean isNumber(value: string): boolean isPercent(value: string): boolean isTshirtSize(value: string): boolean } ``` An object containing all the validators used in tailwind-merge. They are useful if you want to use a custom config with [`extendTailwindMerge`](#extendtailwindmerge) or [`createTailwindMerge`](#createtailwindmerge). **Example usage**: ```ts const paddingClassGroup = [{ p: [validators.isNumber] }] const customImageGroup = [{ 'custom-img': [validators.isArbitraryImage] }] ``` ### Simple Type Validators These validators check for basic patterns and types: | Validator | Description | Example Match | | ----------------------- | ------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------- | | `isAny` | Always returns `true`. Use carefully - matches everything. Best when certain no other class groups exist in a namespace. | Matches any value | | `isAnyNonArbitrary` | Checks if class part is NOT an arbitrary value or variable | `red`, `lg`, `4` | | `isNamedContainerQuery` | Checks for named container query classes | `@container/sidebar`, `@container-size/main` | | `isInteger` | Matches integer values | `3`, `100` | | `isNumber` | Matches any number (integer or decimal) | `3`, `1.5`, `0.25` | | `isFraction` | Matches fraction patterns | `1/2`, `127/256` | | `isPercent` | Matches percentage values | `12.5%`, `50%` | | `isTshirtSize` | Matches T-shirt sizes, optionally with number prefix | `sm`, `xl`, `2xl` | ### Arbitrary Value Validators These validators check arbitrary values (values in square brackets `[...]`): | Validator | Description | Example Match | Common Use | | ----------------------- | ----------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | --------------------------------- | | `isArbitraryValue` | Checks if value is enclosed in brackets | `[something]` | Generic arbitrary value detection | | `isArbitraryLength` | Checks for arbitrary length values | `[3%]`, `[4px]`, `[length:var(--my-var)]` | Width, height, spacing | | `isArbitraryNumber` | Checks for arbitrary numbers or `number:` labeled values | `[450]`, `[number:var(--value)]` | Font-weight, z-index | | `isArbitraryWeight` | Checks for arbitrary font weight values or `weight:`/`number:` labeled values | `[400]`, `[bold]`, `[weight:var(--fw)]` | Font-weight | | `isArbitraryFamilyName` | Checks for `family-name:` labeled values | `[family-name:Open_Sans]` | Font-family | | `isArbitraryPosition` | Checks for `position:` labeled values | `[position:200px_100px]` | Background-position | | `isArbitrarySize` | Checks for `size:` labeled values | `[size:200px_100px]` | Background-size | | `isArbitraryImage` | Checks for image-like values (starts with `image:`, `url:`, `linear-gradient(`, etc.) | `[url('/path.png')]`, `[image:var(--img)]` | Background-image | | `isArbitraryShadow` | Checks for shadow patterns (two lengths separated by underscore, optionally with `inset`) | `[0_35px_60px_-15px_rgba(0,0,0,0.3)]`, `[inset_0_4px_8px_rgba(0,0,0,0.1)]` | Box-shadow, text-shadow | ### Arbitrary Variable Validators These validators check arbitrary CSS variables (values in parentheses `(...)`): | Validator | Description | Example Match | Common Use | | ------------------------------- | ----------------------------------------------------------------- | --------------------------------------- | -------------------- | | `isArbitraryVariable` | Checks if value is a CSS variable in parentheses | `(--my-var)` | Generic CSS variable | | `isArbitraryVariableLength` | Checks for variables with `length` label | `(length:--my-length)` | Length properties | | `isArbitraryVariableSize` | Checks for variables with `size`, `length`, or `percentage` label | `(size:--my-size)` | Size properties | | `isArbitraryVariablePosition` | Checks for variables with `position` label | `(position:--my-position)` | Position properties | | `isArbitraryVariableImage` | Checks for variables with `image` or `url` label | `(image:--my-image)` | Image properties | | `isArbitraryVariableFamilyName` | Checks for variables with `family-name` label | `(family-name:--my-font)` | Font-family | | `isArbitraryVariableWeight` | Checks for variables with `weight` or `number` label, or no label | `(weight:--my-fw)`, `(--my-weight)` | Font-weight | | `isArbitraryVariableShadow` | Checks for variables with `shadow` label or no label | `(shadow:--my-shadow)`, `(--my-shadow)` | Shadow properties | ### Usage Examples ```ts import { extendTailwindMerge, validators } from 'tailwind-merge' const twMerge = extendTailwindMerge({ extend: { classGroups: { // Custom size classes that accept numbers or fractions 'custom-size': [{ size: [validators.isNumber, validators.isFraction] }], // Custom image classes 'hero-image': [{ 'hero-img': [validators.isArbitraryImage] }], // Custom spacing with T-shirt sizes 'custom-gap': [{ gap: [validators.isTshirtSize] }], // Accept any value (use with caution) 'theme-color': [{ theme: [validators.isAny] }], }, }, }) ``` ## `Config` ```ts interface Config { … } ``` TypeScript type for config object. Useful if you want to build a `createConfig` function but don't want to define it inline in [`extendTailwindMerge`](#extendtailwindmerge) or [`createTailwindMerge`](#createtailwindmerge). ## `DefaultClassGroupIds` ```ts type DefaultClassGroupIds = 'accent' | 'align-content' | 'align-items' | … ``` TypeScript type for all class group IDs defined in the default config of tailwind-merge. ## `DefaultThemeGroupIds` ```ts type DefaultThemeGroupIds = 'blur' | 'borderColor' | 'borderRadius' | … ``` TypeScript type for all theme group IDs defined in the default config of tailwind-merge. ## `ClassNameValue` ```ts type ClassNameValue = string | null | undefined | 0 | false | ClassNameValue[] ``` TypeScript type for arguments accepted by [`twMerge`](#twmerge) and [`twJoin`](#twjoin). You might want to use it if you wrap any of those with your own function. ```ts function myWrappedTwMerge(...args: ClassNameValue[]) { doSomething() return twMerge(...args) } ``` ## `ClassValidator` ```ts type ClassValidator = (value: string) => boolean ``` TypeScript type for class validators accepted in class definitions within [`extendTailwindMerge`](#extendtailwindmerge) and [`createTailwindMerge`](#createtailwindmerge). --- Next: [Writing plugins](./writing-plugins.md) Previous: [Recipes](./recipes.md) [Back to overview](./README.md) dcastil-tailwind-merge-c8c57aa/docs/changelog/000077500000000000000000000000001520007772200214215ustar00rootroot00000000000000dcastil-tailwind-merge-c8c57aa/docs/changelog/v0-changelog.md000066400000000000000000000267071520007772200242310ustar00rootroot00000000000000# Changelog for v0 releases ## v0.9.0 ### New Features - Mimic theme from Tailwind config in tailwind-merge config by [@dcastil](https://github.com/dcastil) in [#55](https://github.com/dcastil/tailwind-merge/pull/55) - Adds support for some theme properties like `spacing`, `borderRadius`, etc. More on that in the [theme docs](https://github.com/dcastil/tailwind-merge/tree/v0.9.0#theme). **Full Changelog**: [`v0.8.2...v0.9.0`](https://github.com/dcastil/tailwind-merge/compare/v0.8.2...v0.9.0) ## v0.8.2 ### Bug Fixes - Fix custom values for list-style-type missing in default config by [@dcastil](https://github.com/dcastil) in [#53](https://github.com/dcastil/tailwind-merge/pull/53) **Full Changelog**: [`v0.8.1...v0.8.2`](https://github.com/dcastil/tailwind-merge/compare/v0.8.1...v0.8.2) ## v0.8.1 ### Bug Fixes - Fix missing support for custom values in some class groups in default config by [@dcastil](https://github.com/dcastil) in [#52](https://github.com/dcastil/tailwind-merge/pull/52) - Adds missing support for custom values in following class groups: Grid Auto Columns, Grid Auto Rows, Max-Width, Transition Property, Transition Timing Function, Animation, Fill, Stroke **Full Changelog**: [`v0.8.0...v0.8.1`](https://github.com/dcastil/tailwind-merge/compare/v0.8.0...v0.8.1) ## v0.8.0 ### Summary This is a big release with breaking changes. These were necessary to improve the API before the coming v1.0.0 release and to add better plugin support. If you feel like writing a plugin, check out the [plugins section](https://github.com/dcastil/tailwind-merge/tree/v0.8.0#writing-plugins) in the docs! ### Breaking Changes - Previously there was a list of prefixes in the tailwind-merge config which was used to match Tailwind prefixes. I removed those and match all prefixes as valid Tailwind prefixes. This is only a breaking change if you use non-Tailwind classes with `:` in them which look like Tailwind classes after the prefix. tailwind-merge will treat those as Tailwind classes and resolve conflicts incorrectly. [#50](https://github.com/dcastil/tailwind-merge/pull/50)) - `createTailwindMerge` - If you added custom prefixes to your tailwind-merge config, you can remove them now. If you use TypeScript, you need to remove them. ([#50](https://github.com/dcastil/tailwind-merge/pull/50)) - Doesn't get passed the `getDefaultConfig` callback anymore. You need to call the exported function [`getDefaultConfig`](https://github.com/dcastil/tailwind-merge/tree/v0.8.0#getdefaultconfig) instead to get the default config. ([#42](https://github.com/dcastil/tailwind-merge/pull/42)) - You probably don't need `createTailwindMerge` anyway. Use the new and much simpler to use [`extendTailwindMerge`](https://github.com/dcastil/tailwind-merge/tree/v0.8.0#extendtailwindmerge) instead. ([#49](https://github.com/dcastil/tailwind-merge/pull/49)) ### New Features - Add plugin documentation by [@dcastil](https://github.com/dcastil) in [#51](https://github.com/dcastil/tailwind-merge/pull/51) - (Breaking change) Consider all prefixes as valid Tailwind prefixes by [@dcastil](https://github.com/dcastil) in [#50](https://github.com/dcastil/tailwind-merge/pull/50) - Add `extendTailwindMerge` function by [@dcastil](https://github.com/dcastil) in [#49](https://github.com/dcastil/tailwind-merge/pull/49) - Add `mergeConfigs` function by [@dcastil](https://github.com/dcastil) in [#45](https://github.com/dcastil/tailwind-merge/pull/45) - (Breaking change) Code-split default config out by [@dcastil](https://github.com/dcastil) in [#42](https://github.com/dcastil/tailwind-merge/pull/42) - Enable multiple `createConfig` functions by [@dcastil](https://github.com/dcastil) in [#40](https://github.com/dcastil/tailwind-merge/pull/40) - Add validators to package exports by [@dcastil](https://github.com/dcastil) in [#38](https://github.com/dcastil/tailwind-merge/pull/38) ### Bug Fixes - Remove unwated side effects when mutating default config by [@dcastil](https://github.com/dcastil) in [#43](https://github.com/dcastil/tailwind-merge/pull/43) - Also added the `Config` type to the package exports **Full Changelog**: [`v0.7.1...v0.8.0`](https://github.com/dcastil/tailwind-merge/compare/v0.7.1...v0.8.0) ## v0.7.1 ### Bug Fixes - Fix CommonJS imports not working by [@navin-moorthy](https://github.com/navin-moorthy) and [@dcastil](https://github.com/dcastil) in [#31](https://github.com/dcastil/tailwind-merge/pull/31) **Full Changelog**: [`v0.7.0...v0.7.1`](https://github.com/dcastil/tailwind-merge/compare/v0.7.0...v0.7.1) ## v0.7.0 ### New Features - Allow passing `false` to `twMerge` by [@dcastil](https://github.com/dcastil) in [#25](https://github.com/dcastil/tailwind-merge/pull/25) **Full Changelog**: [`v0.6.0...v0.7.0`](https://github.com/dcastil/tailwind-merge/compare/v0.6.0...v0.7.0) ## v0.6.0 ### New Features - Allow passing `null` as argument to `twMerge` by [@dcastil](https://github.com/dcastil) in [#20](https://github.com/dcastil/tailwind-merge/pull/20) ### Bug Fixes - Fix merges with important modifier not working when using prefixed classes by [@dcastil](https://github.com/dcastil) in [#22](https://github.com/dcastil/tailwind-merge/pull/22) **Full Changelog**: [`v0.5.2...v0.6.0`](https://github.com/dcastil/tailwind-merge/compare/v0.5.2...v0.6.0) ## v0.5.2 ### Bug Fixes - Add support for custom classes like `cursor-[grab]` by [@dcastil](https://github.com/dcastil) in [#19](https://github.com/dcastil/tailwind-merge/pull/19) - Fix incorrect conflicts in Font Variant Numeric classes by [@dcastil](https://github.com/dcastil) in [#18](https://github.com/dcastil/tailwind-merge/pull/18) **Full Changelog**: [`v0.5.1...v0.5.2`](https://github.com/dcastil/tailwind-merge/compare/v0.5.1...v0.5.2) ## v0.5.1 ### Bug Fixes - Fix incorrect conflict between ring and shadow by [@dcastil](https://github.com/dcastil) in [#15](https://github.com/dcastil/tailwind-merge/pull/15) - Fix not all JIT-enabled pseudo variants working by [@dcastil](https://github.com/dcastil) in [#14](https://github.com/dcastil/tailwind-merge/pull/14) ### Other - Add CodeQL security analysis to repo by [@dcastil](https://github.com/dcastil) in [#10](https://github.com/dcastil/tailwind-merge/pull/10) **Full Changelog**: [`v0.5.0...v0.5.1`](https://github.com/dcastil/tailwind-merge/compare/v0.5.0...v0.5.1) ## v0.5.0 ### New Features - Add support for Tailwind CSS v2.2.6 by [@dcastil](https://github.com/dcastil) in [#8](https://github.com/dcastil/tailwind-merge/pull/8) - Delay `twMerge` initialization until first call by [@dcastil](https://github.com/dcastil) in [#7](https://github.com/dcastil/tailwind-merge/pull/7) ### Other - Make logo in README version-specific by [@dcastil](https://github.com/dcastil) in [#6](https://github.com/dcastil/tailwind-merge/pull/6) **Full Changelog**: [`v0.4.0...v0.5.0`](https://github.com/dcastil/tailwind-merge/compare/v0.4.0...v0.5.0) ## v0.4.0 ### Breaking Changes - `createTailwindMerge` - Config type changed and returned object from `getDefaultConfig` changed. ([#3](https://github.com/dcastil/tailwind-merge/pull/3)) - `dynamicClasses` and `standaloneClasses` were merged into `classGroups` - `conflictingGroups` were renamed to `conflictingClassGroups` - Class group IDs are keys of `classGroups` object instead of full path to class group. E.g. `dynamicClasses.foo.0` → `foo`. ### New Features - (Breaking change) Simplify config by [@dcastil](https://github.com/dcastil) in [#3](https://github.com/dcastil/tailwind-merge/pull/3) - Add support for Tailwind v2.2.5 by [@dcastil](https://github.com/dcastil) in [#2](https://github.com/dcastil/tailwind-merge/pull/2) ### Bug Fixes - Fix non-conflicting border classes being merged incorrectly by [@dcastil](https://github.com/dcastil) in [#5](https://github.com/dcastil/tailwind-merge/pull/5) **Full Changelog**: [`v0.3.0...v0.4.0`](https://github.com/dcastil/tailwind-merge/compare/v0.3.0...v0.4.0) ## v0.3.0 ### New Features - Enable named class group collections by [@dcastil](https://github.com/dcastil) in [`16a3175`](https://github.com/dcastil/tailwind-merge/commit/16a31751fdfd87c39683fb4506dec6a0bf118772) - Class group collections in config can be an object, so you can use group references like `dynamicClasses.flex.direction` instead of just index-based ones like `dynamicClasses.flex.0`. More info on this in the [`createTailwindMerge()` API reference](https://github.com/dcastil/tailwind-merge/tree/v0.3.0#createtailwindmerge). ### Bug Fixes - Fix small typos in README by [@KeKs0r](https://github.com/KeKs0r) in [#1](https://github.com/dcastil/tailwind-merge/pull/1) ### Other - tailwind-merge now has a logo by [@dcastil](https://github.com/dcastil) in [`9402c8e`](https://github.com/dcastil/tailwind-merge/commit/9402c8ec1b19714bb8ecb158eb124b89a8c1ba64), [`b969979`](https://github.com/dcastil/tailwind-merge/commit/b96997942d4e92e045c9b08e7314843305b41fe2), [`dae2aaa`](https://github.com/dcastil/tailwind-merge/commit/dae2aaabf8c00b0d5086aff744aa2c53513daf33), [`e9e8806`](https://github.com/dcastil/tailwind-merge/commit/e9e88060578483a3ed4728430f7b188e51aaef49), [`4e2d9f1`](https://github.com/dcastil/tailwind-merge/commit/4e2d9f160dae00e18ebcbecf54f4dd33dc4264bb)

tailwind-merge **Full Changelog**: [`v0.2.0...v0.3.0`](https://github.com/dcastil/tailwind-merge/compare/v0.2.0...v0.3.0) ## v0.2.0 ### New Features - Add support for important modifier by [@dcastil](https://github.com/dcastil) in [`17a9e9d`](https://github.com/dcastil/tailwind-merge/commit/17a9e9d9d805ba585372e01a9eefae1dba89368b) - Add support for per-side border colors by [@dcastil](https://github.com/dcastil) in [`6cd9ee2`](https://github.com/dcastil/tailwind-merge/commit/6cd9ee255e1a6fb42e5262b7835f876e2ce8e3ae) - Add support for content utiltites by [@dcastil](https://github.com/dcastil) in [`fd097c6`](https://github.com/dcastil/tailwind-merge/commit/fd097c611b5fa02ff6142b66f5ad96d596feed42) - Add support for caret color by [@dcastil](https://github.com/dcastil) in [`fd097c6`](https://github.com/dcastil/tailwind-merge/commit/fd097c611b5fa02ff6142b66f5ad96d596feed42) - Add support for JIT-only prefixes by [@dcastil](https://github.com/dcastil) in [`fd097c6`](https://github.com/dcastil/tailwind-merge/commit/fd097c611b5fa02ff6142b66f5ad96d596feed42) - Mark package as side effect-free by [@dcastil](https://github.com/dcastil) in [`6d00af3`](https://github.com/dcastil/tailwind-merge/commit/6d00af33049c3c7171bce3f1affd561986cc6bfe) ### Bug Fixes - Fix classes like `bottom-auto` not being detected as Tailwind classesby [@dcastil](https://github.com/dcastil) in [`ec65b84`](https://github.com/dcastil/tailwind-merge/commit/ec65b84f0aab57d1305019e4a143772d675798a2) **Full Changelog**: [`v0.1.2...v0.2.0`](https://github.com/dcastil/tailwind-merge/compare/v0.1.2...v0.2.0) ## v0.1.2 ### Bug Fixes - Fix errors when importing tailwind-merge as third party dependency. by [@dcastil](https://github.com/dcastil) in [`3335956`](https://github.com/dcastil/tailwind-merge/commit/3335956da15fdcc484904976864816413fad68f6) **Full Changelog**: [`v0.1.1...v0.1.2`](https://github.com/dcastil/tailwind-merge/compare/v0.1.1...v0.1.2) ## v0.1.1 No changes here. Just testing automated npm publish. **Full Changelog**: [`v0.1.0...v0.1.1`](https://github.com/dcastil/tailwind-merge/compare/v0.1.0...v0.1.1) ## v0.1.0 First published version of tailwind-merge. 🎉 **Full Changelog**: [`v0.1.0`](https://github.com/dcastil/tailwind-merge/commits/v0.1.0) dcastil-tailwind-merge-c8c57aa/docs/changelog/v0-to-v1-migration.md000066400000000000000000000052651520007772200252330ustar00rootroot00000000000000# Guide to migrate from tailwind-merge v0 to v1 This document is only about breaking changes between v0 and v1. For a full list of changes, see the [v1.0.0 release](./v1-changelog.md#v100). ## Overview tailwind-merge v1 drops support for Tailwind CSS v2 and in turn adds support for Tailwind CSS v3. There are no breaking changes in the tailwind-merge types and some breaking changes for a small number of users in the return values, so you should get through smoothly. ## Breaking changes ### `twMerge` and `extendTailwindMerge` - Outline utilities from Tailwind v2 don't get merged anymore since they were replaced by outline width, outline style, outline offset and outline color in Tailwind v3 ([`55ab167`](https://github.com/dcastil/tailwind-merge/commit/55ab167b7167519873c5dd4d258dc62212d1659a), [#63](https://github.com/dcastil/tailwind-merge/pull/63)) - The classes `overflow-ellipsis` and `overflow-clip` will not get merged with class `truncate` anymore, but the new Tailwind v3 classes `text-ellipsis` and `text-clip` will. ([`65b03e4`](https://github.com/dcastil/tailwind-merge/commit/65b03e48914ac5d7d52eea9ec178b204d30609c9), [#63](https://github.com/dcastil/tailwind-merge/pull/63)) - The classes `decoration-slice` and `decoration-clone` won't get merged anymore and `box-decoration-slide` nad `box-decoration-clone` will ([`bfe2cc9`](https://github.com/dcastil/tailwind-merge/commit/bfe2cc9bb221107fa0bf363cc325ddbb04677f43), [#63](https://github.com/dcastil/tailwind-merge/pull/63)) ### `getDefaultConfig` - Removed class group `outline` since it was removed in Tailwind v3 ([`55ab167`](https://github.com/dcastil/tailwind-merge/commit/55ab167b7167519873c5dd4d258dc62212d1659a), [#63](https://github.com/dcastil/tailwind-merge/pull/63)) - Renamed class group `vertival-alignment` (yes, the typo was in the code) to `vertical-align` ([`1269ce6`](https://github.com/dcastil/tailwind-merge/commit/1269ce68ae39807ceadbecc98c0929fdfdb446d0), [#63](https://github.com/dcastil/tailwind-merge/pull/63)) - Renamed class groups `flex-basis`, `flex-grow` and `flex-shrink` to `basis`, `grow` and `shrink` to stay consistent with Tailwind v3 ([`e6d8912`](https://github.com/dcastil/tailwind-merge/commit/e6d8912e47bf9a89346b9b0cc822fb2bff2af172), [#63](https://github.com/dcastil/tailwind-merge/pull/63)) ### `validators` - `isCustomLength` and `isCustomValue` were renamed to `isArbitraryLength` and `isArbitraryValue` to be consistent with naming in Tailwind v3 documentation ([`adc3c02`](https://github.com/dcastil/tailwind-merge/commit/adc3c02c7f035069beec1c62777ec008172587ab), [#63](https://github.com/dcastil/tailwind-merge/pull/63)) ## Steps to upgrade - Upgrade to Tailwind CSS v3 - Upgrade to tailwind-merge v1 dcastil-tailwind-merge-c8c57aa/docs/changelog/v1-changelog.md000066400000000000000000000606461520007772200242320ustar00rootroot00000000000000# Changelog for v1 releases ## v1.14.0 ### New Features - Improve support for arbitrary values by [@dcastil](https://github.com/dcastil) in [#263](https://github.com/dcastil/tailwind-merge/pull/263) - Previously, tailwind-merge checked the content of the arbitrary value portion of a class in most cases to understand whether the type of the arbitrary value is correct (e.g. it checked for number followed by length unit for the length type). That lead to the issue that a class like `mt-[calc(theme(fontSize.4xl)/1.125)]` would not be recognized correctly because no length unit is present in the arbitrary value. - I changed the check of the arbitrary value to also pass when a `calc()`, `min()`, `max()` and `clamp()` function is used in the arbitrary value. - In ambiguous cases, you can use data type labels to mark the type of a class. [Read more](https://github.com/dcastil/tailwind-merge/blob/v1.14.0/docs/features.md#supports-arbitrary-values) - Moreover, I removed the check where the type of the arbitrary value is unambiguous. In the class `mt-[…]` the arbitrary value can only be a length, so I don't check for it. A consequence of this is that if you use non-Tailwind classes like `mt-[this-is-totally-not-tailwind]`, tailwind-merge will recognize it as a Tailwind class from now on. **Please don't use classes that look like Tailwind classes with arbitrary value but aren't Tailwind classes with tailwind-merge (in default config) to prevent incorrect merging behavior**. ### Bug Fixes - Fix `col-span-full` class missing in default config by [@dcastil](https://github.com/dcastil) in [#267](https://github.com/dcastil/tailwind-merge/pull/267) - Fix arbitrary value with no present length unit not being recognized by [@dcastil](https://github.com/dcastil) in [#263](https://github.com/dcastil/tailwind-merge/pull/263) ### Documentation - Add missing info about where to call config functions to docs by [@dcastil](https://github.com/dcastil) in [#264](https://github.com/dcastil/tailwind-merge/pull/264) **Full Changelog**: [`v1.13.2...v1.14.0`](https://github.com/dcastil/tailwind-merge/compare/v1.13.2...v1.14.0) ## v1.13.2 ### Bug Fixes - Fix TypeScript issue when using `moduleResolution: node16` by [@LiamMartens](https://github.com/LiamMartens) in [#245](https://github.com/dcastil/tailwind-merge/pull/245) ### Documentation - Fix grammar and typos in docs by [@jmmarco](https://github.com/jmmarco) in [#249](https://github.com/dcastil/tailwind-merge/pull/249) - Add tailwind-merge-php to similar packages in docs by [@JamesHemery](https://github.com/JamesHemery) in [#246](https://github.com/dcastil/tailwind-merge/pull/246) **Full Changelog**: [`v1.13.1...v1.13.2`](https://github.com/dcastil/tailwind-merge/compare/v1.13.1...v1.13.2) Thanks to [@bluetch](https://github.com/bluetch) for sponsoring tailwind-merge! ❤️ ## v1.13.1 ### Bug Fixes - Fix `basis-auto` and `basis-full` not being merged correctly by [@lukasz-kapica](https://github.com/lukasz-kapica) in [#242](https://github.com/dcastil/tailwind-merge/pull/242) **Full Changelog**: [`v1.13.0...v1.13.1`](https://github.com/dcastil/tailwind-merge/compare/v1.13.0...v1.13.1) ## v1.13.0 This release focuses on improvements to the docs. ### Bug Fixes - Fix arbitrary length `0` without unit not being recognized by [@dcastil](https://github.com/dcastil) in [#237](https://github.com/dcastil/tailwind-merge/pull/237) - Fix typo in comment in default-config.ts by [@CrutchTheClutch](https://github.com/CrutchTheClutch) in [#227](https://github.com/dcastil/tailwind-merge/pull/227) ### Documentation - Add intro video from Simon Vrachliotis to docs by [@dcastil](https://github.com/dcastil) in [#239](https://github.com/dcastil/tailwind-merge/pull/239) - [@simonswiss](https://github.com/simonswiss) made an intro video to tailwind-merge which I added to the docs. Check it out [here](https://github.com/dcastil/tailwind-merge/blob/v1.13.0/docs/what-is-it-for.md#video-introduction)! - Add docs about when to use tailwind-merge by [@dcastil](https://github.com/dcastil) in [#230](https://github.com/dcastil/tailwind-merge/pull/230) - I added a new page to the docs about [when and how to use tailwind-merge](https://github.com/dcastil/tailwind-merge/blob/v1.13.0/docs/when-and-how-to-use-it.md) which should help you to decide whether you should use it and what alternative approaches exist. - Make it clearer in docs that tailwind-merge supports composition of class strings by [@dcastil](https://github.com/dcastil) in [#229](https://github.com/dcastil/tailwind-merge/pull/229) ### Other - Add npm package provenance by [@dcastil](https://github.com/dcastil) in [#219](https://github.com/dcastil/tailwind-merge/pull/219) - GitHub introduced a new security feature to verify which source commit and build file were used for a specific npm package version. tailwind-merge now publishes provenance signatures alongside all releases on npm. [Read more on the GitHub blog.](https://github.blog/2023-04-19-introducing-npm-package-provenance/) **Full Changelog**: [`v1.12.0...v1.13.0`](https://github.com/dcastil/tailwind-merge/compare/v1.12.0...v1.13.0) ## v1.12.0 With this release tailwind-merge supports all features introduced in [Tailwind CSS v3.3](https://tailwindcss.com/blog/tailwindcss-v3-3). ### New Features - Add support for postfix modifier by [@dcastil](https://github.com/dcastil) in [#214](https://github.com/dcastil/tailwind-merge/pull/214) - This adds support for `line-height` modifiers in `font-size` utilities like `text-lg/7` and more potential postfix modifiers in the future ([learn more](https://github.com/dcastil/tailwind-merge/blob/v1.12.0/docs/features.md#supports-postfix-modifiers)). - All classes are checked for postfix modifiers since there will be more in the future and they'll be configurable with plugins. - tailwind-merge can't know from the class syntax alone whether something is a modifier or not. E.g. there is `w-1/2` which doesn't contain a modifier. So tailwind-merge always checks whether class without potential modifier exists and if it doesn't it checks for full class. This behavior might get reversed in the next major version as a breaking change ([learn more](https://github.com/dcastil/tailwind-merge/issues/215)). - Added `conflictingClassGroupModifiers` object to tailwind-merge config ([learn more](https://github.com/dcastil/tailwind-merge/blob/v1.12.0/docs/configuration.md#postfix-modifiers-conflicting-with-class-groups)). **Full Changelog**: [`v1.11.0...v1.12.0`](https://github.com/dcastil/tailwind-merge/compare/v1.11.0...v1.12.0) ## v1.11.0 ### New Features - Add support for Tailwind CSS v3.3 except line-height shorthand by [@dcastil](https://github.com/dcastil) in [#210](https://github.com/dcastil/tailwind-merge/pull/210) - The line-height shorthand in font-size utilities (`text-lg/7`) [introduced in Tailwind CSS v3.3](https://tailwindcss.com/blog/tailwindcss-v3-3#new-line-height-shorthand-for-font-size-utilities) is not yet supported in tailwind-merge because that feature is a bit more involved. I'll add support for it in a future release. More info in [#211](https://github.com/dcastil/tailwind-merge/issues/211). - Added new [validator](https://github.com/dcastil/tailwind-merge/blob/v1.11.0/docs/api-reference.md#validators) `isPercent` which is needed internally for the default scale of color stop positions. - New [theme](https://github.com/dcastil/tailwind-merge/blob/v1.11.0/docs/configuration.md#theme) key `gradientColorStopPositions` supported in tailwind-merge. - New logical properties like `ps-0` (`padding-inline-start: 0px;`) are de-duplicated away when using the matching property for all sides afterwards like in this case `p-0`, but not when using single axis sides like `px-0` because `padding-inline-start` can also be the top or bottom padding depending on writing mode. - Basically `twMerge('ps-0 p-0') === 'p-0' && twMerge('ps-0 px-0') === 'ps-0 px-0'`. - If you want to use logical properties and know which writing modes your app is limited to, add the right conflicts yourself to your tailwind-merge config. **Full Changelog**: [`v1.10.0...v1.11.0`](https://github.com/dcastil/tailwind-merge/compare/v1.10.0...v1.11.0) Thanks to [@brandonmcconnell](https://github.com/brandonmcconnell) for sponsoring tailwind-merge! ❤️ ## v1.10.0 ### New Features - Add support for container query length units in arbitrary values by [@LesnoyPudge](https://github.com/LesnoyPudge) in [#204](https://github.com/dcastil/tailwind-merge/pull/204) **Full Changelog**: [`v1.9.1...v1.10.0`](https://github.com/dcastil/tailwind-merge/compare/v1.9.1...v1.10.0) ## v1.9.1 ### Bug Fixes - Fix arbitrary floats not supported in opacity, scale, etc. by [@dcastil](https://github.com/dcastil) in [#196](https://github.com/dcastil/tailwind-merge/pull/196) - Up until now classes like `opacity-50 opacity-[.025]` weren't merged correctly. You might not have noticed since classes with arbitrary values are defined after the default ones in the output stylesheet, but merging it the other way around (`opacity-[.025] opacity-50`) would have led to a styling bug. - This fix led to a new [validator](https://github.com/dcastil/tailwind-merge/blob/v1.9.1/docs/api-reference.md#validators) `isNumber`. - Fix arbitrary border color value being merged incorrectly by [@dcastil](https://github.com/dcastil) in [#195](https://github.com/dcastil/tailwind-merge/pull/195) **Full Changelog**: [`v1.9.0...v1.9.1`](https://github.com/dcastil/tailwind-merge/compare/v1.9.0...v1.9.1) ## v1.9.0 ### New Features - Support decimals in T-shirt sizes by [@farreldarian](https://github.com/farreldarian) in [#189](https://github.com/dcastil/tailwind-merge/pull/189) ### Documentation - Fixes typo in recipes docs by [@nicklemmon](https://github.com/nicklemmon) in [#181](https://github.com/dcastil/tailwind-merge/pull/181) ### Other - Added test case by [@chuanyu0201](https://github.com/chuanyu0201) in [#186](https://github.com/dcastil/tailwind-merge/pull/186) **Full Changelog**: [`v1.8.1...v1.9.0`](https://github.com/dcastil/tailwind-merge/compare/v1.8.1...v1.9.0) ## v1.8.1 ### Bug Fixes - Fix incorrect class group conflicts in grid-related classes by [@dcastil](https://github.com/dcastil) in [#177](https://github.com/dcastil/tailwind-merge/pull/177) **Full Changelog**: [`v1.8.0...v1.8.1`](https://github.com/dcastil/tailwind-merge/compare/v1.8.0...v1.8.1) Thanks to [@aniravi24](https://github.com/aniravi24) for sponsoring tailwind-merge! ❤️ ## v1.8.0 ### New Features - Add support for custom separator by [@dcastil](https://github.com/dcastil) in [#168](https://github.com/dcastil/tailwind-merge/pull/168) - Add support for dynamic viewport units in arbitrary lengths by [@dcastil](https://github.com/dcastil) in [#166](https://github.com/dcastil/tailwind-merge/pull/166) - Rename `join` to `twJoin` by [@dcastil](https://github.com/dcastil) in [#161](https://github.com/dcastil/tailwind-merge/pull/161) - I deprecated the [`join`](https://github.com/dcastil/tailwind-merge/blob/v1.7.0/docs/api-reference.md#join) function and renamed it to [`twJoin`](https://github.com/dcastil/tailwind-merge/blob/v1.8.0/docs/api-reference.md#twjoin) to make replacing it via search and replace easier since `join` is a common function name. You don't need to change anything right now, but if you have some spare time, rename join → twJoin in your code since the `join` function will be removed in the next major release. **Full Changelog**: [`v1.7.0...v1.8.0`](https://github.com/dcastil/tailwind-merge/compare/v1.7.0...v1.8.0) Thanks to [@gjtorikian](https://github.com/gjtorikian) for sponsoring tailwind-merge! ❤️ ## v1.7.0 ### New Features - Add support for Tailwind CSS v3.2 by [@dcastil](https://github.com/dcastil) and [@brandonmcconnell](https://github.com/brandonmcconnell) in [#159](https://github.com/dcastil/tailwind-merge/pull/159) **Full Changelog**: [`v1.6.2...v1.7.0`](https://github.com/dcastil/tailwind-merge/compare/v1.6.2...v1.7.0) ## v1.6.2 ### Bug Fixes - Fix arbitrary numbers not working in stroke-width by [@dcastil](https://github.com/dcastil) in [#153](https://github.com/dcastil/tailwind-merge/pull/153) - Now tailwind-merge handles classes like `stroke-[3]` correctly. - I deprecated the [validator](https://github.com/dcastil/tailwind-merge/blob/v1.6.2/docs/api-reference.md#validators) `isArbitraryWeight` and renamed it to `isArbitraryNumber` to reflect its broader use case. You don't need to change anything right now, but if you have some spare time, rename `isArbitraryWeight` → `isArbitraryNumber` in your code since `isArbitraryWeight` will be removed in the next major release. ### Other - Add package version number to dev releases by [@dcastil](https://github.com/dcastil) in [#154](https://github.com/dcastil/tailwind-merge/pull/154) - Now dev releases don't start with `0.0.0-dev` anymore and instead have the version number of the last release, like `1.6.1-dev`. That makes it easier for tools like [Renovate](https://renovatebot.com/) to understand which package version you're using in case you use dev releases. **Full Changelog**: [`v1.6.1...v1.6.2`](https://github.com/dcastil/tailwind-merge/compare/v1.6.1...v1.6.2) ## v1.6.1 ### Bug Fixes - Fix h-min not working by [@dcastil](https://github.com/dcastil) in [#146](https://github.com/dcastil/tailwind-merge/pull/146) **Full Changelog**: [`v1.6.0...v1.6.1`](https://github.com/dcastil/tailwind-merge/compare/v1.6.0...v1.6.1) ## v1.6.0 ### New Features - Add support for arrays as argument by [@dcastil](https://github.com/dcastil) in [#127](https://github.com/dcastil/tailwind-merge/pull/127) - You can now use arbitrarily nested arrays as arguments to `twMerge`. That's especially handy for nested conditions. ```js twMerge('…', someBool && ['…', anotherBool && '…']) ``` - The joining of arguments is done with a new `join` function which [is present in the tailwind-merge exports](https://github.com/dcastil/tailwind-merge/blob/v1.6.0/docs/api-reference.md#join) as well. It has the same functionality as [clsx](https://github.com/lukeed/clsx) but without support for objects as arguments which makes it a little faster. - Why no objects as arguments? You can read about my reasoning in [#137 (comment)](https://github.com/dcastil/tailwind-merge/discussions/137#discussioncomment-3481605). ### Bug Fixes - Replace matchAll with exec by [@dcastil](https://github.com/dcastil) in [#133](https://github.com/dcastil/tailwind-merge/pull/133) - This makes tailwind-merge work in older browsers which don't support `String.prototype.matchAll()` ### Other - Add recipes section to docs by [@dcastil](https://github.com/dcastil) in [#134](https://github.com/dcastil/tailwind-merge/pull/134) - Split docs into multiple files by [@dcastil](https://github.com/dcastil) in [#131](https://github.com/dcastil/tailwind-merge/pull/131) - Add comments to released PRs and their related issues by [@dcastil](https://github.com/dcastil) in [#130](https://github.com/dcastil/tailwind-merge/pull/130) - Add tests to check actual CJS and ESM package exports by [@dcastil](https://github.com/dcastil) in [#129](https://github.com/dcastil/tailwind-merge/pull/129) - Remove ts files from npm package by [@dcastil](https://github.com/dcastil) in [#128](https://github.com/dcastil/tailwind-merge/pull/128) **Full Changelog**: [`v1.5.1...v1.6.0`](https://github.com/dcastil/tailwind-merge/compare/v1.5.1...v1.6.0) Thanks to [@charkour](https://github.com/charkour) for sponsoring tailwind-merge! ❤️ ## v1.5.1 ### Bug Fixes - Fix arbitrary z-index value not being recognized by [@dcastil](https://github.com/dcastil) in [#116](https://github.com/dcastil/tailwind-merge/pull/116) **Full Changelog**: [`v1.5.0...v1.5.1`](https://github.com/dcastil/tailwind-merge/compare/v1.5.0...v1.5.1) Thanks to [@charkour](https://github.com/charkour) for sponsoring tailwind-merge! ❤️ ## v1.5.0 ### New Features - Added missing ESM `types` subpath export condition by [@scott-lc](https://github.com/scott-lc) in [#113](https://github.com/dcastil/tailwind-merge/pull/113) - This makes tailwind-merge work in TypeScript projects using [the new `nodenext` module type introduced in TypeScript 4.7](https://devblogs.microsoft.com/typescript/announcing-typescript-4-7/#ecmascript-module-support-in-node-js). **Full Changelog**: [`v1.4.0...v1.5.0`](https://github.com/dcastil/tailwind-merge/compare/v1.4.0...v1.5.0) Thanks to [@charkour](https://github.com/charkour) for sponsoring tailwind-merge! ❤️ ## v1.4.0 ### New Features - Optimize runtime performance by using Map by [@dcastil](https://github.com/dcastil) in [#105](https://github.com/dcastil/tailwind-merge/pull/105) ### Bug Fixes - Fix overriding w-full with w-fit not working by [@dcastil](https://github.com/dcastil) in [#112](https://github.com/dcastil/tailwind-merge/pull/112) ### Other - Publish dev releases of every commit on main branch by [@dcastil](https://github.com/dcastil) in [#104](https://github.com/dcastil/tailwind-merge/pull/104) **Full Changelog**: [`v1.3.0...v1.4.0`](https://github.com/dcastil/tailwind-merge/compare/v1.3.0...v1.4.0) Thanks to [@charkour](https://github.com/charkour) for sponsoring tailwind-merge! ❤️ ## v1.3.0 ### New Features - Add support for Tailwind v3.1 by [@dcastil](https://github.com/dcastil) in [#103](https://github.com/dcastil/tailwind-merge/pull/103) ### Bug Fixes - Fix readme typos by [@oshi97](https://github.com/oshi97) in [#102](https://github.com/dcastil/tailwind-merge/pull/102) - Fix typos in comments by [@szamanr](https://github.com/szamanr) in [#89](https://github.com/dcastil/tailwind-merge/pull/89) ### Other - Add community health files by [@dcastil](https://github.com/dcastil) in [#93](https://github.com/dcastil/tailwind-merge/pull/93) **Full Changelog**: [`v1.2.1...v1.3.0`](https://github.com/dcastil/tailwind-merge/compare/v1.2.1...v1.3.0) Thanks to [@charkour](https://github.com/charkour) for sponsoring tailwind-merge! ❤️ ## v1.2.1 ### Bug Fixes - Fix isArbitraryWeight incorrectly using `weight:` instead of `number:` for disambiguation for arbitrary values by [@liuqiang1357](https://github.com/liuqiang1357) in [#85](https://github.com/dcastil/tailwind-merge/pull/85) - Fix typos in README.md by [@Gri-ffin](https://github.com/Gri-ffin) in [#82](https://github.com/dcastil/tailwind-merge/pull/82) **Full Changelog**: [`v1.2.0...v1.2.1`](https://github.com/dcastil/tailwind-merge/compare/v1.2.0...v1.2.1) Thanks to [@charkour](https://github.com/charkour) for sponsoring tailwind-merge! ❤️ ## v1.2.0 ### New Features - Add prefix support by [@dcastil](https://github.com/dcastil) in [#78](https://github.com/dcastil/tailwind-merge/pull/78) **Full Changelog**: [`v1.1.1...v1.2.0`](https://github.com/dcastil/tailwind-merge/compare/v1.1.1...v1.2.0) Thanks to [@charkour](https://github.com/charkour) for sponsoring tailwind-merge! ❤️ ## v1.1.1 ### Bug Fixes - Fix TypeScript types not being linked correctly in package.json by [@navin-moorthy](https://github.com/navin-moorthy) and [@dcastil](https://github.com/dcastil) in [#75](https://github.com/dcastil/tailwind-merge/pull/75) **Full Changelog**: [`v1.1.0...v1.1.1`](https://github.com/dcastil/tailwind-merge/compare/v1.1.0...v1.1.1) ## v1.1.0 ### New Features - Fix missing arbitrary value support in some class groups by [@dcastil](https://github.com/dcastil) in [#73](https://github.com/dcastil/tailwind-merge/pull/73) - Adds arbitrary value support for class groups `grayscale`, `invert`, `sepia`, `grow`, `shrink`, `object-position`, `shadow`, `drop-shadow`, `rotate`, `skew` and `transform-origin` - Fixes `break-inside` classes being merged incorrectly - Adds missing classes `overline`, `underline-offset`, `content-none` - Fixes typo in class group name `bg-repeeat` → `bg-repeat` - Adds `isArbitraryShadow` validator - Improve tree-shaking by [@dcastil](https://github.com/dcastil) in [#65](https://github.com/dcastil/tailwind-merge/pull/65) - I changed the build output significantly here and removed `"type": "module"` from the package.json. I did test the new npm package output in Node and in the browser, but it's hard to account for every possible build system tailwind-merge is used in. If some issues come up with bundling tailwind-merge, please open an issue! ### Bug Fixes - Fix stroke-color utilities being merged with stroke-width utilities by [@dcastil](https://github.com/dcastil) in [#72](https://github.com/dcastil/tailwind-merge/pull/72) - Fix mix-blend utilities getting merged incorrectly by [@dcastil](https://github.com/dcastil) in [#71](https://github.com/dcastil/tailwind-merge/pull/71) **Full Changelog**: [`v1.0.0...v1.1.0`](https://github.com/dcastil/tailwind-merge/compare/v1.0.0...v1.1.0) ## v1.0.0 v1! 🎉 ### Overview After being 5 months on v0 I think it's time to release a stable version of tailwind-merge. Tailwind v3 was [released](https://github.com/tailwindlabs/tailwindcss/releases/tag/v3.0.0) yesterday and it's no coincidence that I'm releasing tailwind-merge v1 today. I added full support for Tailwind v3 so you can update both at once. There are no breaking changes in the tailwind-merge types and some breaking changes for a small number of users in the return values, so you should get through smoothly. By the way, you can now [sponsor](https://github.com/sponsors/dcastil) this project. 😊 ### Breaking Changes - `twMerge`, `extendTailwindMerge` - Outline utilities from Tailwind v2 don't get merged anymore since they were replaced by outline width, outline style, outline offset and outline color in Tailwind v3 ([`55ab167`](https://github.com/dcastil/tailwind-merge/commit/55ab167b7167519873c5dd4d258dc62212d1659a), [#63](https://github.com/dcastil/tailwind-merge/pull/63)) - The classes `overflow-ellipsis` and `overflow-clip` will not get merged with class `truncate` anymore, but the new Tailwind v3 classes `text-ellipsis` and `text-clip` will. ([`65b03e4`](https://github.com/dcastil/tailwind-merge/commit/65b03e48914ac5d7d52eea9ec178b204d30609c9), [#63](https://github.com/dcastil/tailwind-merge/pull/63)) - The classes `decoration-slice` and `decoration-clone` won't get merged anymore and `box-decoration-slide` nad `box-decoration-clone` will ([`bfe2cc9`](https://github.com/dcastil/tailwind-merge/commit/bfe2cc9bb221107fa0bf363cc325ddbb04677f43), [#63](https://github.com/dcastil/tailwind-merge/pull/63)) - `getDefaultConfig` - Removed class group `outline` since it was removed in Tailwind v3 ([`55ab167`](https://github.com/dcastil/tailwind-merge/commit/55ab167b7167519873c5dd4d258dc62212d1659a), [#63](https://github.com/dcastil/tailwind-merge/pull/63)) - Renamed class group `vertival-alignment` (yes, the typo was in the code) to `vertical-align` ([`1269ce6`](https://github.com/dcastil/tailwind-merge/commit/1269ce68ae39807ceadbecc98c0929fdfdb446d0), [#63](https://github.com/dcastil/tailwind-merge/pull/63)) - Renamed class groups `flex-basis`, `flex-grow` and `flex-shrink` to `basis`, `grow` and `shrink` to stay consistent with Tailwind v3 ([`e6d8912`](https://github.com/dcastil/tailwind-merge/commit/e6d8912e47bf9a89346b9b0cc822fb2bff2af172), [#63](https://github.com/dcastil/tailwind-merge/pull/63)) - `validators` - `isCustomLength` and `isCustomValue` were renamed to `isArbitraryLength` and `isArbitraryValue` to be consistent with naming in Tailwind v3 documentation ([`adc3c02`](https://github.com/dcastil/tailwind-merge/commit/adc3c02c7f035069beec1c62777ec008172587ab), [#63](https://github.com/dcastil/tailwind-merge/pull/63)) ### New Features - Add support for Tailwind v3 by [@dcastil](https://github.com/dcastil) in [#63](https://github.com/dcastil/tailwind-merge/pull/63) - Support for all the new utility classes and variants in Tailwind v3.0.0 - Support for arbitrary properties like `[--my-var:20px]` - Support for important modifiers in arbitrary properties like `![--my-very-important-var: 21px]` - Support for new labels for classes with arbitrary value: `size`, `position`, `url`, `weight` and `family` - New validators `isTshirtSize`, `isArbitrarySize`, `isArbitraryPosition`, `isArbitraryUrl` and `isArbitraryWeight`, check them out in the [documentation](https://github.com/dcastil/tailwind-merge/tree/v1.0.0#validators)! ([`fec2b18`](https://github.com/dcastil/tailwind-merge/commit/fec2b18870f6806e602009632b52b9fe89ebfb83), [`f8acd7c`](https://github.com/dcastil/tailwind-merge/commit/f8acd7ca5be6cc40ad4c1cbdee7522bbc44c7870), [#63](https://github.com/dcastil/tailwind-merge/pull/63)) **Full Changelog**: [`v0.9.0...v1.0.0`](https://github.com/dcastil/tailwind-merge/compare/v0.9.0...v1.0.0) dcastil-tailwind-merge-c8c57aa/docs/changelog/v1-to-v2-migration.md000066400000000000000000000501311520007772200252250ustar00rootroot00000000000000# Guide to migrate from tailwind-merge v1 to v2 This document is only about breaking changes between v1 and v2. For a full list of changes, see the [v2.0.0 release](./v2-changelog.md#v200). ## Overview The tailwind-merge v2 release focuses on making it easier to configure the library for new users. Over the past one and a half years since the v1 release, the biggest source of issues was the initial configuration of the library. That will unfortunately not change since most of the inconvenience comes from keeping the bundle size to a minimum. However, a few ideas with breaking changes accumulated over time to make the configuration a little bit more straight forward without increasing the bundle size. Those ideas were implemented in this release. The highlights: - No changes to `twMerge` and supported Tailwind CSS versions. - You can now override class groups with the object notation in `extendTailwindMerge`. - TypeScript types of configuration object passed to `extendTailwindMerge` are stricter and prevent you from using unknown theme groups and class groups unless specified otherwise. - The library now exports a bundle using modern JS syntax to further reduce its size. There is also an additional bundle with ES5-only syntax for compatibility with older browsers. If you have feedback of any kind regarding this release, feel free to [open an issue or discussion](https://github.com/dcastil/tailwind-merge/issues/new/choose). I'm always happy to hear from you! ## Breaking changes By exports: - All - [Modern JS syntax](#modern-js-syntax) - [Module resolution](#module-resolution) - `extendTailwindMerge` - [Object shape changed](#extendtailwindmerge-object-shape-changed) - [Stricter TypeScript types](#extendtailwindmerge-stricter-typescript-types) - `validators` - [`isLength`: Does not check for arbitrary values anymore](#validatorsislength-does-not-check-for-arbitrary-values-anymore) - [`isInteger`: Does not check for arbitrary values anymore](#validatorsisinteger-does-not-check-for-arbitrary-values-anymore) - [`isArbitraryUrl`: Renamed](#validatorsisarbitraryurl-renamed) - [`isArbitraryWeight`: Removed](#validatorsisarbitraryweight-removed) - `createTailwindMerge` - [Mandatory elements added](#createtailwindmerge-mandatory-elements-added) - `fromTheme` - [Stricter TypeScript types](#fromtheme-stricter-typescript-types) - `mergeConfigs` - [Object shape changed](#mergeconfigs-object-shape-changed) - [Stricter TypeScript types](#mergeconfigs-stricter-typescript-types) - `join` - [Removed](#join-removed) ### Modern JS syntax Related: [#286](https://github.com/dcastil/tailwind-merge/pull/286), [#287](https://github.com/dcastil/tailwind-merge/pull/287) The build of tailwind-merge was updated to output more modern JS syntax. This removes a lot of code to substitute modern JS features like object spread or arrow functions, but also means that the new bundle doesn't work in older browsers anymore in case you don't transpile your dependencies. The exact [browserslist](https://github.com/browserslist/browserslist) query used to define how tailwind-merge gets transpiled from now on is ``` > 0.5%, last 2 versions, Firefox ESR, not dead, maintained node versions ``` which corresponds to the browserslist query `defaults` with the addition of `maintained node versions`. In case you need to support older browsers and don't transpile your dependencies, you can use the new `tailwind-merge/es5` bundle which includes all of the tailwind-merge exports and is transpiled to ES5 syntax using the browserslist query `supports es5`. #### Minimal upgrade Replace all imports of `tailwind-merge` with `tailwind-merge/es5`. ```diff - import { … } from 'tailwind-merge' + import { … } from 'tailwind-merge/es5' ``` #### Full upgrade Check whether the browserslist query `> 0.5%, last 2 versions, Firefox ESR, not dead, maintained node versions` includes all browsers you need to support. If not, replace all imports of `tailwind-merge` with `tailwind-merge/es5` as shown in the minimal upgrade above. ### Module resolution Related: [#289](https://github.com/dcastil/tailwind-merge/issues/289) The `module` field was removed from the `package.json` file of tailwind-merge. This field was briefly introduced by bundlers to import the ES module version of a library. But it was ultimately succeeded by the `exports` field which became a standard in Node.js and is also used by tailwind-merge. All major bundlers support the `exports` field, so removing the `module` field should hopefully not affect anyone. However, if you encounter any problems with this change, please open an issue. #### Upgrade If you can import tailwind-merge and your module resolution system doesn't complain, there is nothing you need to do. If you encounter issues, make sure that you resolve the tailwind-merge library via the `exports` or `main` field of its `package.json` file. ### `extendTailwindMerge`: Object shape changed Related: [#294](https://github.com/dcastil/tailwind-merge/pull/294) In the object passed to `extendTailwindMerge` it is now possible to not just extend groups, but also to override them. To distinguish between extending and overriding, there are two new top level keys in the object: `extend` and `override`. Both of these objects can define a `theme`, `classGroups`, `conflictingClassGroups` and `conflictingClassGroupModifiers` object whose groups will either override or extend the corresponding groups in the default config. You might remember the `extend` key in your `tailwind.config.js`, now it works similarly in tailwind-merge as well. However, you need to use the `override` object instead of defining groups at the top level to override. This was done to make overriding more explicit and to prevent accidentally overriding groups when migrating to tailwind-merge v2. #### Minimal upgrade To get the same behavior as before, move `theme`, `classGroups`, `conflictingClassGroups` and `conflictingClassGroupModifiers` into an `extend` object. ```diff import { extendTailwindMerge } from 'tailwind-merge' const twMerge = extendTailwindMerge({ cacheSize: 1000, - theme: { … }, - classGroups: { … }, - conflictingClassGroups: { … }, - conflictingClassGroupModifiers: { … }, + extend: { + theme: { … }, + classGroups: { … }, + conflictingClassGroups: { … }, + conflictingClassGroupModifiers: { … }, + } }) ``` #### Full upgrade If you override groups in your `tailwind.config.js`, you can now do the same in tailwind-merge. Move the groups you want to override into the `override` object and the rest into the `extend` object. ```diff import { extendTailwindMerge } from 'tailwind-merge' const twMerge = extendTailwindMerge({ - classGroups: { - // Here we actually meant to override this group - shadow: [{ shadow: ['none', '100', '200', '300'] }], - // Here we really meant to extend this group - animate: ['animate-shimmer'], - }, + override: { + classGroups: { + // Now we can override this group + shadow: [{ shadow: ['none', '100', '200', '300'] }], + }, + }, + extend: { + classGroups: { + // And we can still extend this group + animate: ['animate-shimmer'], + }, + }, }) ``` ### `extendTailwindMerge`: Stricter TypeScript types Related: [#279](https://github.com/dcastil/tailwind-merge/pull/279) One of the more annoying things about configuring tailwind-merge is to find the right IDs of groups to edit. Some users also fall into the trap that [not all theme groups from Tailwind CSS are supported in tailwind-merge](../configuration.md#theme). To add some guidance to the process, all the group objects passed to `extendTailwindMerge` don't accept any string keys in TypeScript anymore but only a set of specific keys which are defined in the default config. ```ts import { extendTailwindMerge } from 'tailwind-merge' const twMerge = extendTailwindMerge({ extend: { classGroups: { // ↓ No problem shadow: [{ shadow: ['100', '200', '300'] }], // ↓ Error: Object literal may only specify known properties, and 'animation' does not // exist in type 'Partial>>'. animation: ['animate-shimmer'], }, }, }) ``` This means you can't accidentally extend or override a group with a typo in the ID anymore without noticing and you get code completion when you start typing the ID of a group in your editor. But this also means that you can't add any new groups to the configuration object anymore. To allow custom class group IDs, you can use generic arguments to `extendTailwindMerge` to pass additional allowed IDs for class groups and theme groups separately. ```ts import { extendTailwindMerge } from 'tailwind-merge' const twMerge = extendTailwindMerge< // ↓ Add additional class group IDs as the first generic argument 'class-a' | 'class-b', // ↓ Optionally, you can add additional theme group IDs as the second generic argument 'theme-c' | 'theme-d' >({ extend: { theme: { // ↓ No problem since we defined 'theme-c' as allowed theme group ID 'theme-c': […], // ↓ No problem since we defined 'theme-d' as allowed theme group ID 'theme-d': […], }, classGroups: { // ↓ No problem since it's part of the default allowed class group IDs shadow: […], // ↓ No problem since we defined 'class-a' as allowed class group ID 'class-a': […], // ↓ No problem since we defined 'class-b' as allowed class group ID 'class-b': […], }, }, }) ``` #### Minimal upgrade To get the the same behavior as before, allow any group IDs by passing `string` as generic arguments to `extendTailwindMerge`. ```diff import { extendTailwindMerge } from 'tailwind-merge' - const twMerge = extendTailwindMerge({ + const twMerge = extendTailwindMerge({ extend: { theme: { … }, classGroups: { … }, }, }) ``` #### Full upgrade If you aren't using any custom groups, there is nothing you need to do to upgrade. If you use any custom class group or theme group IDs in your tailwind-merge config, pass them as generic arguments to `extendTailwindMerge`. ```diff import { extendTailwindMerge } from 'tailwind-merge' + type AdditionalClassGroupIds = 'class-a' | 'class-b' + type AdditionalThemeGroupIds = 'theme-c' | 'theme-d' - const twMerge = extendTailwindMerge({ + const twMerge = extendTailwindMerge({ // your config }) ``` You can also pass `never` to any argument if you don't use any custom groups of that type. If you still see TypeScript errors on object keys of class groups or theme groups, check in the [default config](../../src/lib/default-config.ts) whether the group ID is defined there. It might be that you configured tailwind-merge incorrectly. ### `validators.isLength`: Does not check for arbitrary values anymore Related: [#292](https://github.com/dcastil/tailwind-merge/pull/292) To be consistent with other validators and to make validators more composable, each validator only checks for class parts with either arbitrary values or non-arbitrary values. The `isLength` validator previously checked for both which made it inconsistent with the rest. Now, `isLength` does not check for arbitrary lengths anymore. #### Upgrade Compose the `isLength` and `isArbitraryLength` validators to get the same behavior as before. You can use one of several options: ```diff import { validators } from 'tailwind-merge' + function isLengthOrArbitraryLength(value: string) { + return validators.isLength(value) || validators.isArbitraryLength(value) + } - validators.isLength + isLengthOrArbitraryLength ``` ```diff import { validators } from 'tailwind-merge' const twMerge = createTailwindMerge(() => ({ cacheSize: 500, separator: ':', theme: {}, classGroups: { - mySpecialGroup: [{ special: [validators.isLength] }], + mySpecialGroup: [{ special: [validators.isLength, validators.isArbitraryLength] }], }, conflictingClassGroups: {}, conflictingClassGroupModifiers: {}, })) ``` ### `validators.isInteger`: Does not check for arbitrary values anymore Related: [#292](https://github.com/dcastil/tailwind-merge/pull/292) Same as the `isLength` validator: To be consistent with other validators and to make validators more composable, each validator only checks for class parts with either arbitrary values or non-arbitrary values. The `isInteger` validator previously checked for both which made it inconsistent with the rest. Now, `isInteger` does not check for arbitrary integers anymore. #### Minimal upgrade tailwind-merge doesn't export an `isArbitraryInteger` validator because it doesn't use one in the default config (it uses `isArbitraryValue` instead because there are no ambiguous arbitrary values in the default config). You'll need to rebuild the previous functionality. ```diff import { validators } from 'tailwind-merge' + function isIntegerOrArbitraryInteger(value: string) { + return validators.isInteger(value) || /^\[(number:.+|-?\d+)\]$/.test(value) + } - validators.isInteger + isIntegerOrArbitraryInteger ``` #### Full upgrade If the classes where you previously used `validators.isInteger` don't use arbitrary values, you can continue to use it without any changes. If those classes use arbitrary values but there is only a single class group that could use the arbitrary value, you don't need to check that the arbitrary value is an integer, only that it is any arbitrary value. E.g. for the class `px-[]` we don't need to know what is between the brackets because only the `px` class group uses the `px-` prefix. In that case you can compose `isInteger` and `isArbitraryValue` as shown in the upgrade section for `validators.isLength`. Otherwise, proceed as shown in the minimal upgrade. ### `validators.isArbitraryUrl`: Renamed Related: [#300](https://github.com/dcastil/tailwind-merge/pull/300) `isArbitraryUrl` was used to detect arbitrary `background-image` values. However, the `background-image` CSS property supports more than just URLs, so the functionality of the validator was expanded to also detect values like `image:var(--maybe-an-image-at-runtime)]` or `linear-gradient(rgba(0,0,255,0.5),rgba(255,255,0,0.5))` and therefore renamed as well. #### Upgrade Replace all uses of `validators.isArbitraryUrl` with `validators.isArbitraryImage`. ```diff import { validators } from 'tailwind-merge' - validators.isArbitraryUrl + validators.isArbitraryImage ``` ### `validators.isArbitraryWeight`: Removed Related: [#288](https://github.com/dcastil/tailwind-merge/pull/288) The validator was renamed to `validators.isArbitraryNumber` in `v1.6.2` to better reflect what it's doing. `validators.isArbitraryWeight` has been deprecated since then and was removed in this release. #### Upgrade Replace all uses of `validators.isArbitraryWeight` with `validators.isArbitraryNumber`. ```diff import { validators } from 'tailwind-merge' - validators.isArbitraryWeight + validators.isArbitraryNumber ``` ### `createTailwindMerge`: Mandatory elements added Related: [#290](https://github.com/dcastil/tailwind-merge/pull/290), [#291](https://github.com/dcastil/tailwind-merge/pull/291) The `separator` and `conflictingClassGroupModifiers` keys became mandatory in the configuration object returned in the `createTailwindMerge` callback since tailwind-merge defines those defaults only in its default config. This change makes processing for plugins easier. #### Upgrade If you don't have those keys defined in the configuration object, add them with their default values. ```diff import { createTailwindMerge } from 'tailwind-merge' const twMerge = createTailwindMerge(() => { return { cacheSize: 500, + separator: ':', theme: {}, classGroups: {}, conflictingClassGroups: {}, + conflictingClassGroupModifiers: {}, } }) ``` ### `fromTheme`: Stricter TypeScript types Related: [#279](https://github.com/dcastil/tailwind-merge/pull/279) Matching the stricter types in `extendTailwindMerge`, the `fromTheme` function now only accepts theme group IDs that are defined in the default config. You can define additional allowed theme group IDs by passing them as generic arguments to `fromTheme`. If you don't use the default config, you can also redefine the default theme group IDs by passing them as a second generic arguments to `fromTheme`. #### Minimal upgrade To get the the same behavior as before, allow any theme group IDs by passing `string` as generic arguments to `fromTheme`. ```diff import { fromTheme } from 'tailwind-merge' - const myThemeGroup = fromTheme('my-theme-group') + const myThemeGroup = fromTheme('my-theme-group') ``` #### Full upgrade If you don't use any custom theme groups, there is nothing you need to do to upgrade. If you use any custom theme groups in your tailwind-merge config, pass them as a generic argument to `fromTheme`. ```diff import { fromTheme } from 'tailwind-merge' + type AdditionalThemeGroupIds = 'my-theme-group' | 'my-other-theme-group' - const customThemeGroup = fromTheme('my-theme-group') + const customThemeGroup = fromTheme('my-theme-group') ``` In case you're not extending the default config but defining a config from scratch with `createTailwindMerge`, you can also redefine the default theme group IDs by passing them as the second generic arguments to `fromTheme`. ```diff import { fromTheme } from 'tailwind-merge' + type ThemeGroupIds = 'my-theme-group' | 'my-other-theme-group' - const customThemeGroup = fromTheme('my-theme-group') + const customThemeGroup = fromTheme('my-theme-group') ``` ### `mergeConfigs`: Object shape changed Related: [#279](https://github.com/dcastil/tailwind-merge/pull/279) `mergeConfigs` is used internally within `extendTailwindMerge`, therefore the object shape change in `extendTailwindMerge` applies to `mergeConfigs` as well. Please read [`extendTailwindMerge`: Object shape changed](#extendtailwindmerge-object-shape-changed) for the details and upgrade instructions. ### `mergeConfigs`: Stricter TypeScript types Related: [#279](https://github.com/dcastil/tailwind-merge/pull/279) The reason and effect of the stricter TypeScript types is the same as in [`extendTailwindMerge`: Stricter TypeScript types](#extendtailwindmerge-stricter-typescript-types). The main difference in `mergeConfigs` is that the generic arguments don't accept additional group IDs but rather all allowed group IDs since `mergeConfigs` is independent of the default config. #### Minimal upgrade To get the the same behavior as before, allow any group IDs by passing `string` as generic arguments to `mergeConfigs`. ```diff import { mergeConfigs } from 'tailwind-merge' - mergeConfigs(baseConfig, { + mergeConfigs(baseConfig, { extend: { theme: { … }, classGroups: { … }, } }) ``` #### Full upgrade You need to define all the class group IDs and theme group IDs you use in two string literal union types and pass them as generic arguments to `mergeConfigs` unless TypeScript inference can infer the types. ```diff import { mergeConfigs } from 'tailwind-merge' + type ClassGroupIds = 'class-a' | 'class-b' + type ThemeGroupIds = 'theme-c' | 'theme-d' - mergeConfigs(baseConfig, { + mergeConfigs(baseConfig, { extend: { theme: { … }, classGroups: { … }, } }) ``` ### `join`: Removed Related: [#288](https://github.com/dcastil/tailwind-merge/pull/288) The `join` function was renamed to `twJoin` in `v1.8.0` to distinguish it better from other functions with the same name in your codebase. `join` has been deprecated since then and was removed in this release. #### Upgrade Replace all uses of `join` with `twJoin`. ```diff - import { join } from 'tailwind-merge' + import { twJoin } from 'tailwind-merge' - join + twJoin ``` dcastil-tailwind-merge-c8c57aa/docs/changelog/v2-changelog.md000066400000000000000000000402451520007772200242240ustar00rootroot00000000000000# Changelog for v2 releases ## v2.6.0 ### New Features - Export ConfigExtension type from package by [@dcastil](https://github.com/dcastil) in [#505](https://github.com/dcastil/tailwind-merge/pull/505) **Full Changelog**: [`v2.5.5...v2.6.0`](https://github.com/dcastil/tailwind-merge/compare/v2.5.5...v2.6.0) Thanks to [@brandonmcconnell](https://github.com/brandonmcconnell), [@manavm1990](https://github.com/manavm1990), [@langy](https://github.com/langy), [@jamesreaco](https://github.com/jamesreaco), [@roboflow](https://github.com/roboflow), [@syntaxfm](https://github.com/syntaxfm), [@getsentry](https://github.com/getsentry), [@codecov](https://github.com/codecov), [@sourcegraph](https://github.com/sourcegraph), a private sponsor and more via [@thnxdev](https://github.com/thnxdev) for sponsoring tailwind-merge! ❤️ ## v2.5.5 ### Bug Fixes - Fix typo "fractons" instead of "fractions" in "stacked-fractions" class by [@oliverhaas](https://github.com/oliverhaas) in [#492](https://github.com/dcastil/tailwind-merge/pull/492) ### Documentation - Add installation instructions to configuration docs by [@dcastil](https://github.com/dcastil) in [#486](https://github.com/dcastil/tailwind-merge/pull/486) **Full Changelog**: [`v2.5.4...v2.5.5`](https://github.com/dcastil/tailwind-merge/compare/v2.5.4...v2.5.5) Thanks to [@brandonmcconnell](https://github.com/brandonmcconnell), [@manavm1990](https://github.com/manavm1990), [@langy](https://github.com/langy), [@jamesreaco](https://github.com/jamesreaco), [@roboflow](https://github.com/roboflow), [@syntaxfm](https://github.com/syntaxfm), [@getsentry](https://github.com/getsentry), [@codecov](https://github.com/codecov) and more via [@thnxdev](https://github.com/thnxdev) for sponsoring tailwind-merge! ❤️ ## v2.5.4 ### Bug Fixes - Fix incorrect paths within sourcemaps by [@dcastil](https://github.com/dcastil) in [#483](https://github.com/dcastil/tailwind-merge/pull/483) **Full Changelog**: [`v2.5.3...v2.5.4`](https://github.com/dcastil/tailwind-merge/compare/v2.5.3...v2.5.4) Thanks to [@brandonmcconnell](https://github.com/brandonmcconnell), [@manavm1990](https://github.com/manavm1990), [@langy](https://github.com/langy), [@jamesreaco](https://github.com/jamesreaco), [@roboflow](https://github.com/roboflow) and [@codecov](https://github.com/codecov) for sponsoring tailwind-merge! ❤️ ## v2.5.3 ### Bug Fixes - Add missing logical border color properties by [@sherlockdoyle](https://github.com/sherlockdoyle) in [#478](https://github.com/dcastil/tailwind-merge/pull/478) ### Documentation - Add benchmark reporting to PRs and commits by [@XantreDev](https://github.com/XantreDev) in [#455](https://github.com/dcastil/tailwind-merge/pull/455) ### Other - Switch test suite to vitest by [@dcastil](https://github.com/dcastil) in [#461](https://github.com/dcastil/tailwind-merge/pull/461) **Full Changelog**: [`v2.5.2...v2.5.3`](https://github.com/dcastil/tailwind-merge/compare/v2.5.2...v2.5.3) Thanks to [@brandonmcconnell](https://github.com/brandonmcconnell), [@manavm1990](https://github.com/manavm1990), [@langy](https://github.com/langy), [@jamesreaco](https://github.com/jamesreaco), [@roboflow](https://github.com/roboflow), [@xeger](https://github.com/xeger) and [@MrDeatHHH](https://github.com/MrDeatHHH) for sponsoring tailwind-merge! ❤️ ## v2.5.2 ### Bug Fixes - Fix multiline input not working anymore by [@dcastil](https://github.com/dcastil) in [#459](https://github.com/dcastil/tailwind-merge/pull/459) **Full Changelog**: [`v2.5.1...v2.5.2`](https://github.com/dcastil/tailwind-merge/compare/v2.5.1...v2.5.2) Thanks to [@brandonmcconnell](https://github.com/brandonmcconnell), [@manavm1990](https://github.com/manavm1990), [@langy](https://github.com/langy), [@jamesreaco](https://github.com/jamesreaco) and [@jamaluddinrumi](https://github.com/jamaluddinrumi) for sponsoring tailwind-merge! ❤️ ## v2.5.1 ### Bug Fixes - Fix space at beginning of input causing infinite loop by [@dcastil](https://github.com/dcastil) in [#457](https://github.com/dcastil/tailwind-merge/pull/457) **Full Changelog**: [`v2.5.0...v2.5.1`](https://github.com/dcastil/tailwind-merge/compare/v2.5.0...v2.5.1) Thanks to [@brandonmcconnell](https://github.com/brandonmcconnell), [@manavm1990](https://github.com/manavm1990), [@langy](https://github.com/langy), [@jamesreaco](https://github.com/jamesreaco) and [@jamaluddinrumi](https://github.com/jamaluddinrumi) for sponsoring tailwind-merge! ❤️ ## v2.5.0 ### New Features - Performance improvements in mergeClassList by [@XantreDev](htttps://github.com/XantreDev) in [#450](https://github.com/dcastil/tailwind-merge/pull/450) and [@rortan134](https://github.com/rortan134) in [#445](https://github.com/dcastil/tailwind-merge/pull/445) - Use arrow functions where possible to help with minification by [@VIKTORVAV99](htttps://github.com/VIKTORVAV99) in [#449](https://github.com/dcastil/tailwind-merge/pull/449) ### Bug FIxes - Fix bg-opacity arbitrary percentages not being recognized properly by [@dcastil](https://github.com/dcastil) in [#451](https://github.com/dcastil/tailwind-merge/pull/451) **Full Changelog**: [`v2.4.0...v2.5.0`](https://github.com/dcastil/tailwind-merge/compare/v2.4.0...v2.5.0) Thanks to [@brandonmcconnell](https://github.com/brandonmcconnell), [@manavm1990](https://github.com/manavm1990), [@langy](https://github.com/langy) and [@jamesreaco](https://github.com/jamesreaco) for sponsoring tailwind-merge! ❤️ ## v2.4.0 ### New Features - Allow hooking into class parsing logic (experimental) by [@dcastil](https://github.com/dcastil) in [#444](https://github.com/dcastil/tailwind-merge/pull/444) - There is no info to this in the docs because this is experimental, but there is a new `experimentalParseClassName` property in the config that allows you to customize how tailwind-merge recognizes classes. If you're interested, you can read how to use it in the [inline JSDocs](https://github.com/dcastil/tailwind-merge/pull/444/files#diff-b8b77f5be18cf56dca425b3a5b8e9d2e754dd37fe0e3612b95cd4e9bccda08a5) and subscribe to https://github.com/dcastil/tailwind-merge/issues/385 for upcoming more powerful low-level functionality. - Create security policy by [@dcastil](https://github.com/dcastil) in [#439](https://github.com/dcastil/tailwind-merge/pull/439) - Added documentation on how to report potential vulnerabilities - Avoid `@babel/runtime` dependency by [@dcastil](https://github.com/dcastil) in [#431](https://github.com/dcastil/tailwind-merge/pull/431) - Now no dependencies in tailwind-merge anymore. This dependency was only used in the `tailwind-merge/es5` bundle anyway which I don't optimize for. ### Documentation - Comment/typo fixes by [@barvian](https://github.com/barvian) in [#443](https://github.com/dcastil/tailwind-merge/pull/443) - Fix typo in doc/recipes.md by [@dsernst](https://github.com/dsernst) in [#428](https://github.com/dcastil/tailwind-merge/pull/428) - Add tailwind-merge-dotnet to similar packages by [@desmondinho](https://github.com/desmondinho) in [#415](https://github.com/dcastil/tailwind-merge/pull/415) ### Other - Added GitHub Action that adds context-v2 label to every issue, discussion and PR by [@Pritam1211](https://github.com/Pritam1211) in [#434](https://github.com/dcastil/tailwind-merge/pull/434) - Replace size-limit action with own metrics-report action by [@dcastil](https://github.com/dcastil) in [#433](https://github.com/dcastil/tailwind-merge/pull/433) **Full Changelog**: [`v2.3.0...v2.4.0`](https://github.com/dcastil/tailwind-merge/compare/v2.3.0...v2.4.0) Thanks to [@brandonmcconnell](https://github.com/brandonmcconnell), [@manavm1990](https://github.com/manavm1990), [@langy](https://github.com/langy), [@jamesreaco](https://github.com/jamesreaco) and [@microsoft](https://github.com/microsoft) for sponsoring tailwind-merge! ❤️ ## v2.3.0 ### New Features - Add support for mix-blend-plus-darker utility by [@dcastil](https://github.com/dcastil) in [#403](https://github.com/dcastil/tailwind-merge/pull/403) - Added support for bigint in ClassNameValue type by [@LarsArtmann](https://github.com/LarsArtmann) in [#402](https://github.com/dcastil/tailwind-merge/pull/402) ### Documentation - Add tailwind-merge-go to similar packages in docs by [@Oudwins](https://github.com/Oudwins) in [#396](https://github.com/dcastil/tailwind-merge/pull/396) **Full Changelog**: [`v2.2.2...v2.3.0`](https://github.com/dcastil/tailwind-merge/compare/v2.2.2...v2.3.0) Thanks to [@brandonmcconnell](https://github.com/brandonmcconnell), [@manavm1990](https://github.com/manavm1990), [@langy](https://github.com/langy) and [@jamesreaco](https://github.com/jamesreaco) for sponsoring tailwind-merge! ❤️ ## v2.2.2 ### Bug Fixes - Fix arbitrary shadow with inset not recognized by [@dcastil](https://github.com/dcastil) in [#392](https://github.com/dcastil/tailwind-merge/pull/392) ### Documentation - Fix minor typos in `configuration.md` by [@stephan281094](https://github.com/stephan281094) in [#372](https://github.com/dcastil/tailwind-merge/pull/372) **Full Changelog**: [`v2.2.1...v2.2.2`](https://github.com/dcastil/tailwind-merge/compare/v2.2.1...v2.2.2) Thanks to [@brandonmcconnell](https://github.com/brandonmcconnell), [@manavm1990](https://github.com/manavm1990), [@langy](https://github.com/langy), a private sponsor and [@jamesreaco](https://github.com/jamesreaco) for sponsoring tailwind-merge! ❤️ ## v2.2.1 ### Bug Fixes - Fix stroke-color being incorrectly detected as stroke-width by [@dcastil](https://github.com/dcastil) in [#371](https://github.com/dcastil/tailwind-merge/pull/371) **Full Changelog**: [`v2.2.0...v2.2.1`](https://github.com/dcastil/tailwind-merge/compare/v2.2.0...v2.2.1) Thanks to [@brandonmcconnell](https://github.com/brandonmcconnell), [@manavm1990](https://github.com/manavm1990) and [@langy](https://github.com/langy) for sponsoring tailwind-merge! ❤️ ## v2.2.0 ### New Features - Add support for Tailwind CSS v3.4 by [@dcastil](https://github.com/dcastil) in [#360](https://github.com/dcastil/tailwind-merge/pull/360) **Full Changelog**: [`v2.1.0...v2.2.0`](https://github.com/dcastil/tailwind-merge/compare/v2.1.0...v2.2.0) Thanks to [@brandonmcconnell](https://github.com/brandonmcconnell), [@manavm1990](https://github.com/manavm1990), [@ErwinAI](https://github.com/ErwinAI) and [@langy](https://github.com/langy) for sponsoring tailwind-merge! ❤️ ## v2.1.0 ### New Features - Add `ClassValidator` type to package exports by [@dcastil](https://github.com/dcastil) in [#348](https://github.com/dcastil/tailwind-merge/pull/348) ### Bug Fixes - Fix display class not removed when it precedes line-clamp class by [@dcastil](https://github.com/dcastil) in [#347](https://github.com/dcastil/tailwind-merge/pull/347) ### Documentation - Fix curly bracket typo in config docs by [@Kosai106](https://github.com/Kosai106) in [#349](https://github.com/dcastil/tailwind-merge/pull/349) **Full Changelog**: [`v2.0.0...v2.1.0`](https://github.com/dcastil/tailwind-merge/compare/v2.0.0...v2.1.0) Thanks to [@brandonmcconnell](https://github.com/brandonmcconnell), [@manavm1990](https://github.com/manavm1990), [@ErwinAI](https://github.com/ErwinAI) and [@langy](https://github.com/langy) for sponsoring tailwind-merge! ❤️ ## v2.0.0 The tailwind-merge v2 release has been sitting here almost finished for 2 months already. But the timing was never quite right, especially thinking about the increased support needed after the release. In the meantime, the product of the company I work at [launched in public beta](https://medium.com/@risecal/meet-rise-the-calendar-that-works-for-you-were-launching-the-public-beta-today-bc07555a2191) and I married. Thank you for your patience. This release focuses on making it easier to configure the library for new users. Check out the [migration guide](./v1-to-v2-migration.md) and if you have any questions, feel free to [create an issue](https://github.com/dcastil/tailwind-merge/issues/new/choose). ### Breaking Changes - Fix `background-image` and `background-position` being merged incorrectly by [@dcastil](https://github.com/dcastil) in [#300](https://github.com/dcastil/tailwind-merge/pull/300) - Values for `background-position` and `background-size` can look very similar and Tailwind CSS uses the same `bg-` prefix for both groups. This results in some [limitations](../limitations.md) for tailwind-merge. If you use background position or background size with arbitrary values, please read [this section](../limitations.md#you-need-to-use-label-in-arbitrary-background-position-and-background-size-classes) about how to use them. - Make types of available keys more strict and configurable through generics by [@kachkaev](https://github.com/kachkaev) in [#279](https://github.com/dcastil/tailwind-merge/pull/279) - Make it possible to override elements with `extendTailwindMerge` by [@dcastil](https://github.com/dcastil) in [#294](https://github.com/dcastil/tailwind-merge/pull/294) - Separate validators better by [@dcastil](https://github.com/dcastil) in [#292](https://github.com/dcastil/tailwind-merge/pull/292) - Make `conflictingClassGroupModifiers` in config non-optional by [@dcastil](https://github.com/dcastil) in [#291](https://github.com/dcastil/tailwind-merge/pull/291) - Move separator to config by [@dcastil](https://github.com/dcastil) in [#290](https://github.com/dcastil/tailwind-merge/pull/290) - Remove `module` field from package.json by [@dcastil](https://github.com/dcastil) in [#289](https://github.com/dcastil/tailwind-merge/pull/289) - Remove deprecated exports by [@dcastil](https://github.com/dcastil) in [#288](https://github.com/dcastil/tailwind-merge/pull/288) - Transpile lib to more modern JS by [@dcastil](https://github.com/dcastil) in [#287](https://github.com/dcastil/tailwind-merge/pull/287) ### New Features - Make types of available keys more strict and configurable through generics by [@kachkaev](https://github.com/kachkaev) in [#279](https://github.com/dcastil/tailwind-merge/pull/279) - Make it possible to override elements with `extendTailwindMerge` by [@dcastil](https://github.com/dcastil) in [#294](https://github.com/dcastil/tailwind-merge/pull/294) - Separate validators better by [@dcastil](https://github.com/dcastil) in [#292](https://github.com/dcastil/tailwind-merge/pull/292) - Make `conflictingClassGroupModifiers` in config non-optional by [@dcastil](https://github.com/dcastil) in [#291](https://github.com/dcastil/tailwind-merge/pull/291) - Move separator to config by [@dcastil](https://github.com/dcastil) in [#290](https://github.com/dcastil/tailwind-merge/pull/290) - Remove `module` field from package.json by [@dcastil](https://github.com/dcastil) in [#289](https://github.com/dcastil/tailwind-merge/pull/289) - Remove deprecated exports by [@dcastil](https://github.com/dcastil) in [#288](https://github.com/dcastil/tailwind-merge/pull/288) - Transpile lib to more modern JS by [@dcastil](https://github.com/dcastil) in [#287](https://github.com/dcastil/tailwind-merge/pull/287) - Add ES5 bundle by [@dcastil](https://github.com/dcastil) in [#286](https://github.com/dcastil/tailwind-merge/pull/286) ### Bug Fixes - Fix touch action classes overriding each other incorrectly by [@dcastil](https://github.com/dcastil) in [#313](https://github.com/dcastil/tailwind-merge/pull/313) - Fix `background-image` and `background-position` being merged incorrectly by [@dcastil](https://github.com/dcastil) in [#300](https://github.com/dcastil/tailwind-merge/pull/300) - Fix number validators accidentally returning `true` for empty strings by [@dcastil](https://github.com/dcastil) in [#295](https://github.com/dcastil/tailwind-merge/pull/295) - Fix typo in test filename by [@CrutchTheClutch](https://github.com/CrutchTheClutch) in [#284](https://github.com/dcastil/tailwind-merge/pull/284) ### Documentation - Explain limitations of arbitrary values in `bg-size` and `bg-position` class groups in docs by [@dcastil](https://github.com/dcastil) in [#328](https://github.com/dcastil/tailwind-merge/pull/328) **Full Changelog**: [`v1.14.0...v2.0.0`](https://github.com/dcastil/tailwind-merge/compare/v1.14.0...v2.0.0) Thanks to [@quezlatch](https://github.com/quezlatch), [@brandonmcconnell](https://github.com/brandonmcconnell), [@manavm1990](https://github.com/manavm1990) and [@ErwinAI](https://github.com/ErwinAI) for sponsoring tailwind-merge! ❤️ dcastil-tailwind-merge-c8c57aa/docs/changelog/v2-to-v3-migration.md000066400000000000000000000274111520007772200252340ustar00rootroot00000000000000# Guide to migrate from tailwind-merge v2 to v3 This document is only about breaking changes between v2 and v3. For a full list of changes, see the [v3.0.0 release](./v3-changelog.md#v300). ## Overview tailwind-merge v3 drops support for Tailwind CSS v3 and in turn adds support for Tailwind CSS v4. That means you should upgrade to Tailwind CSS v4 and tailwind-merge v3 together. All breaking changes are related to the Tailwind CSS v4 support. The highlights: - No breaking changes to `twMerge` other than it expecting classes supported in Tailwind CSS v4. - The `theme` scales tailwind-merge supports were changed and now match the Tailwind CSS v4 [theme variable namespace](https://tailwindcss.com/docs/theme#theme-variable-namespaces). If you have feedback of any kind regarding this release, feel free to [open an issue or discussion](https://github.com/dcastil/tailwind-merge/issues/new/choose). I'm always happy to hear from you! ## Breaking changes By exports: - All - [Supported Tailwind CSS version range changes from v3 to v4](#supported-tailwind-css-version-range-changes-from-v3-to-v4) - `extendTailwindMerge` - [Allowed theme keys changed](#extendtailwindmerge-allowed-theme-keys-changed) - [Custom separators are no longer supported](#extendtailwindmerge-and-createtailwindmerge-custom-separators-are-no-longer-supported) - [Prefix defined in config shouldn't include combining character anymore](#extendtailwindmerge-and-createtailwindmerge-prefix-defined-in-config-shouldnt-include-combining-character-anymore) - `createTailwindMerge` - [Custom separators are no longer supported](#extendtailwindmerge-and-createtailwindmerge-custom-separators-are-no-longer-supported) - [Prefix defined in config shouldn't include combining character anymore](#extendtailwindmerge-and-createtailwindmerge-prefix-defined-in-config-shouldnt-include-combining-character-anymore) - [Mandatory field `orderSensitiveModifiers` added](#createtailwindmerge-mandatory-field-ordersensitivemodifiers-added) - `validators` - [`isLength`: Removed](#validatorsislength-removed) - `DefaultThemeGroupIds` - [Type union changed literals](#defaultthemegroupids-type-union-changed-literals) ### Supported Tailwind CSS version range changes from v3 to v4 tailwind-merge v3.0.0 is expected to be used together with Tailwind CSS v4. That means only features that work in Tailwind CSS v4 are supported. If you intend to stay on Tailwind CSS v3, don't upgrade to tailwind-merge v3.0.0 because there are some breaking changes in expected class syntax. ### `extendTailwindMerge`: Allowed theme keys changed In Tailwind CSS v3 tailwind-merge was in a difficult position with Tailwind's theme scales. If tailwind-merge implemented support for all theme scales, the bundle size would become twice as large. If it didn't support any theme scales, users would have had a difficult time extending scales that are used in many class groups, like in the case of the `spacing` scale. tailwind-merge went with an awkward middle ground where it only supported theme scales that were used in multiple class groups. Tailwind CSS v4 reduced the number of theme scales to only 18 (called [theme variable namespaces](https://tailwindcss.com/docs/theme#theme-variable-namespaces) now) which makes it possible for tailwind-merge to support all of them. That means if you add the CSS variable `--inset-shadow-deep` to your Tailwind CSS theme, you can add it to the `theme` object in tailwind-merge like in the following example: ```ts import { extendTailwindMerge } from 'tailwind-merge' const twMerge = extendTailwindMerge({ extend: { theme: { 'inset-shadow': ['deep'], }, }, }) ``` #### Upgrade Here's how to proceed with upgrading each individual theme scale that got removed or renamed. Just keep in mind that theme scales should only define the custom part of a class (i.e. `deep` for the class `inset-shadow-deep`) whereas class groups should define the whole class (i.e. `inset-shadow-deep` for the class `inset-shadow-deep`). Sometimes you might find that values you configured manually before are now supported by Tailwind CSS v4 by default in which case you can remove them from your tailwind-merge config. | Theme scale in tailwind-merge v2 | What to use in tailwind-merge v3 instead | | -------------------------------- | -------------------------------------------------------------------------- | | `borderColor` | theme scale `color` | | `borderRadius` | theme scale `radius` | | `borderSpacing` | theme scale `spacing` or class groups `border-spacing-*` | | `borderWidth` | class groups `border-w-*` | | `brightness` | class groups `brightness` and `backdrop-brightness` | | `colors` | theme scale `color` | | `contrast` | class groups `contrast` and `backdrop-contrast` | | `gap` | class groups `gap-*` | | `gradientColorStopPositions` | class groups `gradient-from-pos`, `gradient-via-pos` and `gradient-to-pos` | | `gradientColorStops` | class groups `gradient-from`, `gradient-via` and `gradient-to` | | `grayscale` | class groups `grayscale` and `backdrop-grayscale` | | `hueRotate` | class groups `hue-rotate` and `backdrop-hue-rotate` | | `inset` | theme scale `spacing` or class groups `inset-*` | | `invert` | class groups `invert` and `backdrop-invert` | | `margin` | theme scale `spacing` or class groups `m-*` | | `opacity` | class groups `opacity` and `backdrop-opacity` | | `padding` | theme scale `spacing` or class groups `p-*` | | `saturate` | class groups `saturate` and `backdrop-saturate` | | `scale` | class groups `scale-*` | | `sepia` | class groups `sepia` and `backdrop-sepia` | | `skew` | class groups `skew-*` | | `space` | theme scale `spacing` or class groups `space-*` | | `translate` | theme scale `spacing` or class groups `translate-*` | In case you were modifying class groups in tailwind-merge v2 because their theme scale was previously not implemented, it is highly likely that you can move those definitions to the `theme` object in tailwind-merge v3. E.g. if you previously modified the `font-family` class group to add a custom font class, you can now use `theme.font` instead. Check out the [theme documentation](../configuration.md#theme) for more info. ### `extendTailwindMerge` and `createTailwindMerge`: Custom separators are no longer supported Tailwind CSS v4 removed support for custom separators for modifiers in Tailwind CSS classes and now only the default separator `:` is allowed. Therefore tailwind-merge also removed support for custom separators and doesn't use the `separator` key in its config anymore. If you use TypeScript, you'll see a type error if you try to use the `separator` key in your config. #### Upgrade Remove the `separator` key from your tailwind-merge config. ```diff import { extendTailwindMerge } from 'tailwind-merge' const twMerge = extendTailwindMerge({ - separator: '_', … }) ``` ```diff import { createTailwindMerge } from 'tailwind-merge' const twMerge = createTailwindMerge(() => ({ - separator: '_', … })) ``` ### `extendTailwindMerge` and `createTailwindMerge`: Prefix defined in config shouldn't include combining character anymore In Tailwind CSS v4 the optional prefix syntax of classes changed from being in front of the base class like in `hover:*:tw-class` to looking like the first modifier of a class like in `tw:hover:*:class`. In tailwind-merge v2 you had to specify the prefix including the final dash character `-` that combined the prefix with the base class. Due to the new syntax this combining character changes to `:`. To prevent having to change your config if the combining character changes in the future, tailwind-merge v3 expects you to specify the prefix without the combining character. #### Upgrade Remove the dash character from the prefix in your tailwind-merge config. ```diff import { extendTailwindMerge } from 'tailwind-merge' const twMerge = extendTailwindMerge({ - prefix: 'tw-', + prefix: 'tw', … }) ``` ```diff import { createTailwindMerge } from 'tailwind-merge' const twMerge = createTailwindMerge(() => ({ - prefix: 'tw-', + prefix: 'tw', … })) ``` ### `createTailwindMerge`: Mandatory field `orderSensitiveModifiers` added tailwind-merge v3 adds a new mandatory field `orderSensitiveModifiers` to the config to specify which modifiers should be considered order-sensitive. Learn more about order-sensitive modifiers in the [documentation](../configuration.md#order-sensitive-modifiers). #### Minimal upgrade Add the `orderSensitiveModifiers` field to your tailwind-merge config. ```diff import { createTailwindMerge } from 'tailwind-merge' const twMerge = createTailwindMerge(() => ({ + orderSensitiveModifiers: [], … })) ``` #### Full upgrade If you have any order-sensitive modifiers in Tailwind, add them to the new `orderSensitiveModifiers` field in your tailwind-merge config. ```diff import { createTailwindMerge } from 'tailwind-merge' const twMerge = createTailwindMerge(() => ({ … + orderSensitiveModifiers: ['before', '*', …], })) ``` ### `validators.isLength`: Removed The validator `isLength` was used to check whether a class part is a number (`3`, `1.5`), a fraction (`3/4`), or one of the strings `px`, `full` or `screen`. This was used for most length-based scales but the name was not really precise in explaining what it allows. Due to how length-based scales were changed in Tailwind CSS v4, tailwind-merge v3 now uses `isNumber` and `isFraction` instead depending on what each scale supports and strings like `px`, `full` and `screen` are instead specified manually in the scales themselves. This makes it more obvious what those validators do. #### Minimal upgrade You can rebuild the previous validator yourself. ```ts import { validators } from 'tailwind-merge' function isLength(value: string) { return ( validators.isNumber(value) || validators.isFraction(value) || value === 'full' || value === 'px' || value === 'screen' ) } ``` #### Full upgrade Replace all uses of `validators.isLength` with `validators.isNumber`, `validators.isFraction` and the strings `full`, `px` and `screen` depending on which of those your scale supports. ```diff import { validators } from 'tailwind-merge' - [validators.isLength] + [validators.isNumber, validators.isFraction, 'full', 'px', 'screen'] ``` ### `DefaultThemeGroupIds`: Type union changed literals Due to the default theme keys changing, the string union of `DefaultThemeGroupIds` changed as well. Since this is a TypeScript type, errors in TypeScript will determine how to handle this change. dcastil-tailwind-merge-c8c57aa/docs/changelog/v3-changelog.md000066400000000000000000000341541520007772200242270ustar00rootroot00000000000000# Changelog for v3 releases ## v3.6.0 ### New Features - Add support for Tailwind CSS v4.3 by [@dcastil](https://github.com/dcastil) in [#677](https://github.com/dcastil/tailwind-merge/pull/677) - Add support for readonly array values by [@unional](https://github.com/unional) in [#652](https://github.com/dcastil/tailwind-merge/pull/652) ### Documentation - Fix broken links in README by [@maurer2](https://github.com/maurer2) in [#662](https://github.com/dcastil/tailwind-merge/pull/662) ### Other - Harden internal CI pipeline security by omitting git checkout by [@dcastil](https://github.com/dcastil), suggested by [@kyletaylored](https://github.com/kyletaylored) in [`6b2499c`](https://github.com/dcastil/tailwind-merge/commit/6b2499c10cf52bed42426d30b4219e90374b30d6) **Full Changelog**: [`v3.5.0...v3.6.0`](https://github.com/dcastil/tailwind-merge/compare/v3.5.0...v3.6.0) Thanks to [@brandonmcconnell](https://github.com/brandonmcconnell), [@manavm1990](https://github.com/manavm1990), [@langy](https://github.com/langy), [@roboflow](https://github.com/roboflow), [@syntaxfm](https://github.com/syntaxfm), [@getsentry](https://github.com/getsentry), [@codecov](https://github.com/codecov), a private sponsor, [@block](https://github.com/block), [@openclaw](https://github.com/openclaw), [@sourcegraph](https://github.com/sourcegraph), [@mike-healy](https://github.com/mike-healy) and more via [@thnxdev](https://github.com/thnxdev) for sponsoring tailwind-merge! ❤️ ## v3.5.0 ### New Features - Add support for Tailwind CSS v4.2 by [@dcastil](https://github.com/dcastil) in [#651](https://github.com/dcastil/tailwind-merge/pull/651) **Full Changelog**: [`v3.4.1...v3.5.0`](https://github.com/dcastil/tailwind-merge/compare/v3.4.1...v3.5.0) Thanks to [@brandonmcconnell](https://github.com/brandonmcconnell), [@manavm1990](https://github.com/manavm1990), [@langy](https://github.com/langy), [@roboflow](https://github.com/roboflow), [@syntaxfm](https://github.com/syntaxfm), [@getsentry](https://github.com/getsentry), [@codecov](https://github.com/codecov), a private sponsor, [@block](https://github.com/block), [@openclaw](https://github.com/openclaw), [@sourcegraph](https://github.com/sourcegraph) and more via [@thnxdev](https://github.com/thnxdev) for sponsoring tailwind-merge! ❤️ ## v3.4.1 ### Bug Fixes - Prevent arbitrary font-family and font-weight from merging by [@roneymoon](https://github.com/roneymoon) in [#635](https://github.com/dcastil/tailwind-merge/pull/635) **Full Changelog**: [`v3.4.0...v3.4.1`](https://github.com/dcastil/tailwind-merge/compare/v3.4.0...v3.4.1) Thanks to [@brandonmcconnell](https://github.com/brandonmcconnell), [@manavm1990](https://github.com/manavm1990), [@langy](https://github.com/langy), [@roboflow](https://github.com/roboflow), [@syntaxfm](https://github.com/syntaxfm), [@getsentry](https://github.com/getsentry), [@codecov](https://github.com/codecov), a private sponsor, [@block](https://github.com/block), [@openclaw](https://github.com/openclaw), [@sourcegraph](https://github.com/sourcegraph) and more via [@thnxdev](https://github.com/thnxdev) for sponsoring tailwind-merge! ❤️ ## v3.4.0 ### New Features - Performance optimizations which make tailwind-merge >10% faster - Vibe optimization by [@quantizor](https://github.com/quantizor) in [#547](https://github.com/dcastil/tailwind-merge/pull/547) - Additional optimizations by [@quantizor](https://github.com/quantizor) in [#619](https://github.com/dcastil/tailwind-merge/pull/619) ### Documentation - Improve docs by clarifying things, adding more examples by [@dcastil](https://github.com/dcastil) in [#618](https://github.com/dcastil/tailwind-merge/pull/618) - Make examples more realistic by [@dcastil](https://github.com/dcastil) in [#617](https://github.com/dcastil/tailwind-merge/pull/617) - Add custom variant as an alternative to docs by [@kidonng](https://github.com/kidonng) in [#592](https://github.com/dcastil/tailwind-merge/pull/592) ### Other - Improve benchmarking suite by [@quantizor](https://github.com/quantizor) in [#620](https://github.com/dcastil/tailwind-merge/pull/620) **Full Changelog**: [`v3.3.1...v3.4.0`](https://github.com/dcastil/tailwind-merge/compare/v3.3.1...v3.4.0) Thanks to [@brandonmcconnell](https://github.com/brandonmcconnell), [@manavm1990](https://github.com/manavm1990), [@langy](https://github.com/langy), [@roboflow](https://github.com/roboflow), [@syntaxfm](https://github.com/syntaxfm), [@getsentry](https://github.com/getsentry), [@codecov](https://github.com/codecov) and a private sponsor for sponsoring tailwind-merge! ❤️ ## v3.3.1 ### Bug Fixes - Fix arbitrary value using `color-mix()` not being detected as color by [@dcastil](https://github.com/dcastil) in [#591](https://github.com/dcastil/tailwind-merge/pull/591) **Full Changelog**: [`v3.3.0...v3.3.1`](https://github.com/dcastil/tailwind-merge/compare/v3.3.0...v3.3.1) Thanks to [@brandonmcconnell](https://github.com/brandonmcconnell), [@manavm1990](https://github.com/manavm1990), [@langy](https://github.com/langy), [@roboflow](https://github.com/roboflow), [@syntaxfm](https://github.com/syntaxfm), [@getsentry](https://github.com/getsentry), [@codecov](https://github.com/codecov), [@sourcegraph](https://github.com/sourcegraph), a private sponsor, [@block](https://github.com/block) and [shawt3000](https://github.com/shawt3000) for sponsoring tailwind-merge! ❤️ ## v3.3.0 ### New Features - Add support for tailwind CSS v4.1.5 by [@dcastil](https://github.com/dcastil) in [#575](https://github.com/dcastil/tailwind-merge/pull/575) **Full Changelog**: [`v3.2.0...v3.3.0`](https://github.com/dcastil/tailwind-merge/compare/v3.2.0...v3.3.0) Thanks to [@brandonmcconnell](https://github.com/brandonmcconnell), [@manavm1990](https://github.com/manavm1990), [@langy](https://github.com/langy), [@roboflow](https://github.com/roboflow), [@syntaxfm](https://github.com/syntaxfm), [@getsentry](https://github.com/getsentry), [@codecov](https://github.com/codecov), [@sourcegraph](https://github.com/sourcegraph), a private sponsor and [@block](https://github.com/block) for sponsoring tailwind-merge! ❤️ ## v3.2.0 ### New Features - Add support for Tailwind CSS v4.1 by [@dcastil](https://github.com/dcastil) in [#565](https://github.com/dcastil/tailwind-merge/pull/565) **Full Changelog**: [`v3.1.0...v3.2.0`](https://github.com/dcastil/tailwind-merge/compare/v3.1.0...v3.2.0) Thanks to [@brandonmcconnell](https://github.com/brandonmcconnell), [@manavm1990](https://github.com/manavm1990), [@langy](https://github.com/langy), [@jamesreaco](https://github.com/jamesreaco), [@roboflow](https://github.com/roboflow), [@syntaxfm](https://github.com/syntaxfm), [@getsentry](https://github.com/getsentry), [@codecov](https://github.com/codecov), [@sourcegraph](https://github.com/sourcegraph) and a private sponsor for sponsoring tailwind-merge! ❤️ ## v3.1.0 ### New Features - Add support for Tailwind CSS v4.0.10 by [@dcastil](https://github.com/dcastil) in [#546](https://github.com/dcastil/tailwind-merge/pull/546) ### Bug Fixes - Fix length variable in `via-(length:*)` class being merged with `via-` classes accidentally by [@dcastil](https://github.com/dcastil) in [#559](https://github.com/dcastil/tailwind-merge/pull/559) ### Documentation - Fix typo in comment in types.ts by [@roottool](https://github.com/roottool) in [#549](https://github.com/dcastil/tailwind-merge/pull/549) - Update shadow scale recipe to tailwind merge v3 API by [@dcastil](https://github.com/dcastil) in [#545](https://github.com/dcastil/tailwind-merge/pull/545) ### Other - Fix metrics report action erroring on PRs from forks by [@dcastil](https://github.com/dcastil) in [#551](https://github.com/dcastil/tailwind-merge/pull/551) **Full Changelog**: [`v3.0.2...v3.1.0`](https://github.com/dcastil/tailwind-merge/compare/v3.0.2...v3.1.0) Thanks to [@brandonmcconnell](https://github.com/brandonmcconnell), [@manavm1990](https://github.com/manavm1990), [@langy](https://github.com/langy), [@jamesreaco](https://github.com/jamesreaco), [@roboflow](https://github.com/roboflow), [@syntaxfm](https://github.com/syntaxfm), [@getsentry](https://github.com/getsentry), [@codecov](https://github.com/codecov), [@sourcegraph](https://github.com/sourcegraph) and a private sponsor for sponsoring tailwind-merge! ❤️ ## v3.0.2 ### Bug Fixes - Fix `px` value not being recognized for some class groups by [@dcastil](https://github.com/dcastil) in [#538](https://github.com/dcastil/tailwind-merge/pull/538) - Fix doc comment being in incorrect place in default config by [@gjtorikian](https://github.com/gjtorikian) in [#526](https://github.com/dcastil/tailwind-merge/pull/526) **Full Changelog**: [`v3.0.1...v3.0.2](https://github.com/dcastil/tailwind-merge/compare/v3.0.1...v3.0.2) Thanks to [@brandonmcconnell](https://github.com/brandonmcconnell), [@manavm1990](https://github.com/manavm1990), [@langy](https://github.com/langy), [@jamesreaco](https://github.com/jamesreaco), [@roboflow](https://github.com/roboflow), [@syntaxfm](https://github.com/syntaxfm), [@getsentry](https://github.com/getsentry), [@codecov](https://github.com/codecov), [@sourcegraph](https://github.com/sourcegraph) and a private sponsor for sponsoring tailwind-merge! ❤️ ## v3.0.1 ### Bug Fixes - Update info about supported Tailwind CSS version in README by [@dcastil](https://github.com/dcastil) in [`b9c136d`](https://github.com/dcastil/tailwind-merge/commit/b9c136df358ef6012f23bf08258dbf970c0aec43) - Update incorrect link in v3 changelog by [@dcastil](https://github.com/dcastil) in [`e22885e`](https://github.com/dcastil/tailwind-merge/commit/e22885e41e1661f1493f9bf6fb829cfbe1b50281) **Full Changelog**: [`v3.0.0...v3.0.1`](https://github.com/dcastil/tailwind-merge/compare/v3.0.0...v3.0.1) Thanks to [@brandonmcconnell](https://github.com/brandonmcconnell), [@manavm1990](https://github.com/manavm1990), [@langy](https://github.com/langy), [@jamesreaco](https://github.com/jamesreaco), [@roboflow](https://github.com/roboflow), [@syntaxfm](https://github.com/syntaxfm), [@getsentry](https://github.com/getsentry), [@codecov](https://github.com/codecov), [@sourcegraph](https://github.com/sourcegraph) and a private sponsor for sponsoring tailwind-merge! ❤️ ## v3.0.0 [Tailwind CSS v4 is here](https://tailwindcss.com/blog/tailwindcss-v4) and it's time to upgrade tailwind-merge to support it. tailwind-merge v3.0.0 is more accurate than ever and follows the Tailwind CSS spec more closely than in v2. That is thanks to Tailwind CSS v4 being more consistent than ever. This release drops support for Tailwind CSS v3 and in turn adds support for Tailwind CSS v4. That means you should upgrade to Tailwind CSS v4 and tailwind-merge v3 together. All breaking changes are related to the Tailwind CSS v4 support. Check out the [migration guide](./v2-to-v3-migration.md) and if you have any questions, feel free to [create an issue](https://github.com/dcastil/tailwind-merge/issues/new/choose). ### Breaking Changes - Dropping support for Tailwind CSS v3 in favor of support for Tailwind CSS v4 by [@dcastil](https://github.com/dcastil) in [#518](https://github.com/dcastil/tailwind-merge/pull/518) - Theme scales keys changed and now match Tailwind CSS v4 theme variable namespace exactly by [@dcastil](https://github.com/dcastil) in [#518](https://github.com/dcastil/tailwind-merge/pull/518) - `isLength` validator was removed and split into separate validators `isNumber` and `isFraction` by [@dcastil](https://github.com/dcastil) in [#518](https://github.com/dcastil/tailwind-merge/pull/518) - Prefix defined in config shouldn't include combining `-` character anymore by [@dcastil](https://github.com/dcastil) in [#518](https://github.com/dcastil/tailwind-merge/pull/518) - Tailwind CSS v3 prefix position in class not supported anymore in favor of Tailwind CSS v4 position by [@dcastil](https://github.com/dcastil) in [#518](https://github.com/dcastil/tailwind-merge/pull/518) - Custom separators are no longer supported by [@dcastil](https://github.com/dcastil) in [#518](https://github.com/dcastil/tailwind-merge/pull/518) - New mandatory `orderSensitiveModifiers` property in config when using `createTailwindMerge` by [@dcastil](https://github.com/dcastil) in [#518](https://github.com/dcastil/tailwind-merge/pull/518) - `DefaultThemeGroupIds` type union consists of different string literals than before by [@dcastil](https://github.com/dcastil) in [#518](https://github.com/dcastil/tailwind-merge/pull/518) - Classes removed in Tailwind CSS v4 are not supported by tailwind-merge anymore by [@dcastil](https://github.com/dcastil) in [#518](https://github.com/dcastil/tailwind-merge/pull/518) ### New Features - Support for new important modifier position at the end of class by [@dcastil](https://github.com/dcastil) in [#518](https://github.com/dcastil/tailwind-merge/pull/518) - Support for arbitrary CSS variable syntax by [@dcastil](https://github.com/dcastil) in [#518](https://github.com/dcastil/tailwind-merge/pull/518) - There are a bunch of new validators used by tailwind-merge, primarily for new Tailwind CSS v4 features like arbitrary CSS variables by [@dcastil](https://github.com/dcastil) in [#518](https://github.com/dcastil/tailwind-merge/pull/518) ### Bug Fixes - Previously some order-sensitive modifiers like `before:` were treated as not order-sensitive. This is now fixed by [@dcastil](https://github.com/dcastil) in [#518](https://github.com/dcastil/tailwind-merge/pull/518) ### Documentation - Added section explaining order-sensitive modifiers to [configuration docs](../configuration.md#order-sensitive-modifiers) by [@dcastil](https://github.com/dcastil) in [#518](https://github.com/dcastil/tailwind-merge/pull/518) **Full Changelog**: [`v2.6.0...v3.0.0`](https://github.com/dcastil/tailwind-merge/compare/v2.6.0...v3.0.0) Thanks to [@brandonmcconnell](https://github.com/brandonmcconnell), [@manavm1990](https://github.com/manavm1990), [@langy](https://github.com/langy), [@jamesreaco](https://github.com/jamesreaco), [@roboflow](https://github.com/roboflow), [@syntaxfm](https://github.com/syntaxfm), [@getsentry](https://github.com/getsentry), [@codecov](https://github.com/codecov), [@sourcegraph](https://github.com/sourcegraph) and a private sponsor for sponsoring tailwind-merge! ❤️ dcastil-tailwind-merge-c8c57aa/docs/configuration.md000066400000000000000000000520261520007772200226700ustar00rootroot00000000000000# Configuration ## Installation The tailwind-merge package is hosted on npm under the name [`tailwind-merge`](https://www.npmjs.com/package/tailwind-merge). There are lots of package managers for installing packages hosted on npm. Here are installation instructions for the most popular ones: [npm](https://npmjs.com), [yarn](https://yarnpkg.com), [pnpm](https://pnpm.io) and [bun](https://bun.sh). ```sh npm add tailwind-merge yarn add tailwind-merge pnpm add tailwind-merge bun add tailwind-merge ``` ## Basic usage If you're using Tailwind CSS without any extra config, you can use [`twMerge`](./api-reference.md#twmerge) right away. You can safely stop reading the documentation here. ## Usage with custom Tailwind config If you're using a custom Tailwind config, you may need to configure tailwind-merge as well to merge classes properly. The default [`twMerge`](./api-reference.md#twmerge) function is configured in a way that you can still use it if all the following points apply to your Tailwind config: - Only using color names which don't clash with other Tailwind class names - Only deviating by number values from number-based Tailwind classes - Only using font-family classes which don't clash with default font-weight classes - Sticking to default Tailwind config for everything else If some of these points don't apply to you, you can test whether `twMerge` still works as intended with your custom classes. Otherwise, you need create your own custom merge function by either extending the default tailwind-merge config or using a completely custom one. The tailwind-merge config is different from the Tailwind config because it's expected to be shipped and run in the browser as opposed to the Tailwind config which is meant to run at build-time. Be careful in case you're using your Tailwind config directly to configure tailwind-merge in your client-side code because that could result in an unnecessarily large bundle size. ### Shape of tailwind-merge config The tailwind-merge config is an object with a few keys. ```ts const tailwindMergeConfig = { // ↓ Set how many values should be stored in cache. cacheSize: 500, // ↓ Optional prefix from Tailwind config prefix: 'tw', theme: { // Theme scales are defined here }, classGroups: { // Class groups are defined here }, conflictingClassGroups: { // Conflicts between class groups are defined here }, conflictingClassGroupModifiers: { // Conflicts between postfix modifier of a class group and another class group are defined here }, postfixLookupClassGroups: [ // Class group IDs which should be resolved again with their postfix modifier attached ], orderSensitiveModifiers: [ // Modifiers whose order among multiple modifiers should be preserved because their order // changes which element gets targeted. ], } ``` ### Class groups The library uses a concept of _class groups_ which is an array of Tailwind classes which all modify the same CSS property. E.g. here is the position class group. ```ts const positionClassGroup = ['static', 'fixed', 'absolute', 'relative', 'sticky'] ``` tailwind-merge resolves conflicts between classes in a class group and only keeps the last one passed to the merge function call. ```ts twMerge('static sticky relative') // → 'relative' ``` Tailwind classes often share the beginning of the class name, so elements in a class group can also be an object with values of the same shape as a class group (yes, the shape is recursive). In the object each key is joined with all the elements in the corresponding array with a dash (`-`) in between. E.g. here is the overflow class group which results in the classes `overflow-auto`, `overflow-hidden`, `overflow-visible` and `overflow-scroll`. ```ts const overflowClassGroup = [{ overflow: ['auto', 'hidden', 'visible', 'scroll'] }] ``` Sometimes it isn't possible to enumerate all elements in a class group. Think of a Tailwind class which allows arbitrary values. In this scenario you can use a validator function which takes a _class part_ and returns a boolean indicating whether a class is part of a class group. E.g. here is the fill class group. ```ts const isArbitraryValue = (classPart: string) => /^\[.+\]$/.test(classPart) const fillClassGroup = [{ fill: ['current', isArbitraryValue] }] ``` Because the function is under the `fill` key, it will only get called for classes which start with `fill-`. Also, the function only gets passed the part of the class name which comes after `fill-`, this way you can use the same function in multiple class groups. tailwind-merge exports its own [validators](./api-reference.md#validators), so you don't need to recreate them. You can use an empty string (`''`) as a class part if you want to indicate that the preceding part was the end. This is useful for defining elements which are marked as `DEFAULT` in the Tailwind config. ```ts // ↓ Resolves to filter and filter-none const filterClassGroup = [{ filter: ['', 'none'] }] ``` Each class group is defined under its ID in the `classGroups` object in the config. This ID is only used internally, and the only thing that matters is that it is unique among all class groups. ### Conflicting class groups Sometimes there are conflicts across Tailwind classes which are more complex than "remove all those other classes when a class from this group is present in the class list string". One example is the combination of the classes `px-3` (setting `padding-left` and `padding-right`) and `pr-4` (setting `padding-right`). **Scenario 1**: When classes are ordered as `pr-4 px-3`: - You want `px-3` to apply both `padding-left` and `padding-right` - The earlier `pr-4` should be removed since `px-3` also sets `padding-right` - Result: `twMerge('pr-4 px-3') // → 'px-3'` **Scenario 2**: When classes are ordered as `px-3 pr-4`: - You want `px-3` to set `padding-left` - You want `pr-4` to override just the `padding-right` from `px-3` - The `px-3` class should NOT be removed - Result: `twMerge('px-3 pr-4') // → 'px-3 pr-4'` To summarize, `px-3` should stand in conflict with `pr-4`, but `pr-4` should not stand in conflict with `px-3`. To achieve this, we need to define **asymmetric conflicts** across class groups. #### Defining asymmetric conflicts This is what the `conflictingClassGroups` object in the tailwind-merge config is for. You define a key in it which is the ID of a class group which _creates_ a conflict and the value is an array of IDs of class groups which _receive_ a conflict. ```ts const conflictingClassGroups = { // ↓ px creates a conflict with pr and pl px: ['pr', 'pl'], } ``` If a class group _creates_ a conflict, it means that if it appears in a class list string passed to `twMerge`, all preceding class groups in the string which _receive_ the conflict will be removed. When we think of our example, the `px` class group creates a conflict which is received by the class groups `pr` and `pl`. This way `px-3` removes a preceding `pr-4`, but not the other way around. **Common conflict patterns**: | Creates Conflict → | Receives Conflict ← | Example | | ------------------ | ------------------------------------------------------ | ----------------------------------------------- | | `px` | `pl`, `pr` | `twMerge('pr-2 px-4') // → 'px-4'` | | `py` | `pt`, `pb` | `twMerge('pt-2 py-4') // → 'py-4'` | | `p` | `px`, `py`, `pt`, `pr`, `pb`, `pl` | `twMerge('px-2 py-2 p-4') // → 'p-4'` | | `inset` | `top`, `right`, `bottom`, `left`, `inset-x`, `inset-y` | `twMerge('top-0 inset-0') // → 'inset-0'` | | `inset-x` | `left`, `right` | `twMerge('right-0 inset-x-0') // → 'inset-x-0'` | | `inset-y` | `top`, `bottom` | `twMerge('top-0 inset-y-0') // → 'inset-y-0'` | ### Postfix modifiers conflicting with class groups Tailwind CSS allows postfix modifiers for some classes. E.g. you can set font-size and line-height together with `text-lg/7` with `/7` being the postfix modifier. This means that any line-height classes preceding a font-size class with a modifier should be removed. For this tailwind-merge has the `conflictingClassGroupModifiers` object in its config with the same shape as `conflictingClassGroups` explained in the [section above](#conflicting-class-groups). This time the key is the ID of a class group whose modifier _creates_ a conflict and the value is an array of IDs of class groups which _receive_ the conflict. ```ts const conflictingClassGroupModifiers = { 'font-size': ['leading'], } ``` ### Postfix lookup class groups When a class contains a slash (`/`), tailwind-merge first treats the part after the slash as a possible postfix modifier. For example, `text-lg/7` is resolved as the `text-lg` class with `/7` as its postfix modifier. In rare cases, the slash belongs to the full class name instead. The `postfixLookupClassGroups` config property lets you opt in class groups where tailwind-merge should also try resolving the full class name after resolving the part before the slash. ```ts const postfixLookupClassGroups = ['container-type'] ``` This is needed for classes like `@container-size/sidebar`, where `@container-size` is a container type class, but `@container-size/sidebar` is a named container query class which should be resolved as a different class group. ### Order-sensitive modifiers In Tailwind CSS, not all modifiers behave the same when you stack them. In most cases the order of modifiers doesn't matter. E.g. `hover:focus:bg-red-500` and `focus:hover:bg-red-500` behave the same and in the context of tailwind-merge, you'd want them both to override each other. tailwind-merge sorts the modifiers internally to be able to override classes with the same modifiers, even if they are in a different order. However, there are some modifiers where the order matters, e.g. the direct children modifier `*`. The class `*:hover:text-red-500` modifies the text color of a child if that particular child is hovered, but the class `hover:*:text-red-500` modifies the text color of all direct children if the parent is hovered. In this case, you would want tailwind-merge to preserve both classes although they have the same modifiers, just in a different order. To know which modifiers are order-sensitive, tailwind-merge has the `orderSensitiveModifiers` property in its config. `twMerge` is pre-configured with all the order-sensitive modifiers that Tailwind CSS has by default. You'll only need to configure this property if you add your own order-sensitive modifiers or change the meaning of the default order-sensitive modifiers. ### Theme In the Tailwind config you can modify your theme variable namespace to add classes with custom values. tailwind-merge follows the same naming scheme as Tailwind CSS for its theme scales: | Tailwind CSS namespace | tailwind-merge theme key | | ---------------------- | ------------------------ | | `--color-*` | `color` | | `--font-*` | `font` | | `--text-*` | `text` | | `--font-weight-*` | `font-weight` | | `--tracking-*` | `tracking` | | `--leading-*` | `leading` | | `--breakpoint-*` | `breakpoint` | | `--container-*` | `container` | | `--spacing-*` | `spacing` | | `--radius-*` | `radius` | | `--shadow-*` | `shadow` | | `--inset-shadow-*` | `inset-shadow` | | `--drop-shadow-*` | `drop-shadow` | | `--blur-*` | `blur` | | `--perspective-*` | `perspective` | | `--aspect-*` | `aspect` | | `--ease-*` | `ease` | | `--animate-*` | `animate` | If you modified one of the theme namespaces in your Tailwind config, you need to add the variable names to the `theme` object in tailwind-merge as well so that tailwind-merge knows about them. #### Example: Adding custom font sizes Let's say you added a custom font size variable `--text-huge: 100px` to your Tailwind config: ```css /* In your CSS or Tailwind config */ @theme { --text-huge: 100px; } ``` This enables the class `text-huge` in your HTML. To make sure tailwind-merge merges these classes correctly, you need to configure tailwind-merge like this: ```ts import { extendTailwindMerge } from 'tailwind-merge' const customTwMerge = extendTailwindMerge({ extend: { theme: { // ↓ `text` is the key of the namespace `--text-*` // ↓ `huge` is the variable name without the namespace prefix text: ['huge'], }, }, }) // Now tailwind-merge correctly handles your custom font size customTwMerge('text-lg text-huge') // → 'text-huge' customTwMerge('text-huge text-sm') // → 'text-sm' ``` #### Example: Adding custom spacing values For custom spacing in the `--spacing-*` namespace: ```css /* In your CSS or Tailwind config */ @theme { --spacing-gutter: 1.5rem; --spacing-section: 5rem; } ``` Configure tailwind-merge: ```ts const customTwMerge = extendTailwindMerge({ extend: { theme: { spacing: ['gutter', 'section'], }, }, }) // Now works with custom spacing values across all spacing utilities customTwMerge('p-4 p-gutter') // → 'p-gutter' customTwMerge('mt-section mt-4') // → 'mt-4' customTwMerge('gap-2 gap-gutter') // → 'gap-gutter' ``` **Note**: The `spacing` theme scale is used by many utilities including padding (`p-*`), margin (`m-*`), gap (`gap-*`), top/right/bottom/left positioning, and more. Adding a value to the `spacing` theme makes it available across all these utilities. #### Note about custom colors Custom colors in the `--color-*` namespace **do not need to be configured** in tailwind-merge. The library uses a permissive validator that accepts any color name, so custom colors work out of the box: ```css /* In your CSS or Tailwind config */ @theme { --color-brand-primary: #3b82f6; --color-brand-secondary: #8b5cf6; } ``` ```ts import { twMerge } from 'tailwind-merge' // Works without any configuration twMerge('bg-blue-500 bg-brand-primary') // → 'bg-brand-primary' twMerge('text-brand-primary text-brand-secondary') // → 'text-brand-secondary' twMerge('border-custom-color border-brand-primary') // → 'border-brand-primary' ``` This applies to all color utilities: `bg-*`, `text-*`, `border-*`, `ring-*`, etc. ### Extending the tailwind-merge config If you only need to slightly modify the default tailwind-merge config, [`extendTailwindMerge`](./api-reference.md#extendtailwindmerge) is the easiest way to extend the config. You provide it a `configExtension` object which gets [merged](./api-reference.md#mergeconfigs) with the default config. Therefore, all keys here are optional. ```ts import { extendTailwindMerge } from 'tailwind-merge' const twMerge = extendTailwindMerge<'badge' | 'icon-size' | 'card'>({ // ↓ Override elements from the default config // It has the same shape as the `extend` object, so we're going to skip it here. override: {}, // ↓ Extend values from the default config extend: { // ↓ Add values to existing theme scale or create a new one theme: { spacing: ['sm', 'md', 'lg'], }, // ↓ Add values to existing class groups or define new ones classGroups: { badge: ['badge', 'badge-pill', { 'badge-dot': ['', 'sm', 'lg'] }], 'icon-size': [{ icon: ['auto', (value) => Number(value) >= 16] }], card: ['card-sm', 'card-md', 'card-lg'], }, // ↓ Here you can define additional conflicts across class groups conflictingClassGroups: { badge: ['icon-size'], }, // ↓ Define conflicts between postfix modifiers and class groups conflictingClassGroupModifiers: { card: ['icon-size'], }, // ↓ Define order-sensitive modifiers orderSensitiveModifiers: ['custom-variant'], }, }) ``` > [!Note] > The function `extendTailwindMerge` computes a large data structure based on the config passed to it. I recommend to call it only once and store the result in a top-level variable instead of calling it inline within another repeatedly called function. ### TypeScript types for `extendTailwindMerge` If you're using TypeScript, you'll notice that all the places in the `configExtension` object where class group IDs and theme group IDs are used are typed strictly so that you're only allowed to use IDs which are defined in the default config of tailwind-merge. The strict TypeScript types are meant as a safety net to prevent you from making typos in your config and to give you auto-completion for IDs. In case you want to define new class groups or theme objects, you need to add the IDs as a generic argument to `extendTailwindMerge`: ```ts import { extendTailwindMerge } from 'tailwind-merge' type AdditionalClassGroupIDs = 'class-a' | 'class-b' type AdditionalThemeGroupIDs = 'theme-c' | 'theme-d' const twMerge = extendTailwindMerge< // ↓ Add additional class group IDs as the first generic argument AdditionalClassGroupIDs, // ↓ Optionally, you can add additional theme group IDs as the second generic argument AdditionalThemeGroupIDs >({ extend: { theme: { // ↓ Works because we defined 'theme-c' as additional theme group ID 'theme-c': […], // ↓ Works because we defined 'theme-d' as additional theme group ID 'theme-d': […], }, classGroups: { // ↓ Works because it's part of the default additional class group IDs shadow: […], // ↓ Works because we defined 'class-a' as additional class group ID 'class-a': […], // ↓ Works because we defined 'class-b' as additional class group ID 'class-b': […], // ↓ Type […] is not assignable to type […]. // Object literal may only specify known properties, and ''not-defined'' does not exist in type […]. ts(2322) 'not-defined': [], }, }, }) ``` If those strict TypeScript types for IDs are too restrictive for you, you can also allow any strings as IDs by using the `string` type as generic argument. ```ts import { extendTailwindMerge } from 'tailwind-merge' const twMerge = extendTailwindMerge(/* anything goes here */) ``` ### Using completely custom tailwind-merge config If you need to modify the tailwind-merge config and need more control than [`extendTailwindMerge`](./api-reference.md#extendtailwindmerge) gives you or don't want to use the default config (and tree-shake it out of your bundle), you can use [`createTailwindMerge`](./api-reference.md#createtailwindmerge). The function takes a callback which returns the config you want to use and returns a custom `twMerge` function. ```ts import { createTailwindMerge } from 'tailwind-merge' const twMerge = createTailwindMerge(() => ({ cacheSize: 500, theme: {}, classGroups: { badge: ['badge', 'badge-pill', { 'badge-dot': ['', 'sm', 'lg'] }], 'icon-size': [{ icon: ['auto', (value) => Number(value) >= 16] }], card: ['card-sm', 'card-md', 'card-lg'], }, conflictingClassGroups: { badge: ['icon-size'], }, conflictingClassGroupModifiers: { card: ['icon-size'], }, orderSensitiveModifiers: [], })) ``` > [!Note] > The function `createTailwindMerge` computes a large data structure based on the config passed to it. I recommend to call it only once and store the result in a top-level variable instead of calling it inline within another repeatedly called function. The callback passed to `createTailwindMerge` will be called when `twMerge` is called the first time, so you don't need to worry about the computations in it affecting app startup performance in case you aren't using tailwind-merge at app startup. ### Using tailwind-merge plugins You can use both [`extendTailwindMerge`](./api-reference.md#extendtailwindmerge) and [`createTailwindMerge`](./api-reference.md#createtailwindmerge) with third-party plugins. Just add them as arguments after your config. ```ts import { extendTailwindMerge, createTailwindMerge } from 'tailwind-merge' import { withMagic } from 'tailwind-merge-magic-plugin' import { withMoreMagic } from 'tailwind-merge-more-magic-plugin' // With your own config const twMerge1 = extendTailwindMerge({ … }, withMagic, withMoreMagic) // Only using plugin with default config const twMerge2 = extendTailwindMerge(withMagic, withMoreMagic) // Using `createTailwindMerge` const twMerge3 = createTailwindMerge(() => ({ … }), withMagic, withMoreMagic) ``` --- Next: [Recipes](./recipes.md) Previous: [Limitations](./limitations.md) [Back to overview](./README.md) dcastil-tailwind-merge-c8c57aa/docs/contributing.md000066400000000000000000000003231520007772200225210ustar00rootroot00000000000000# Contributing Please see [CONTRIBUTING](../.github/CONTRIBUTING.md) for details. --- Next: [Similar packages](./similar-packages.md) Previous: [Versioning](./versioning.md) [Back to overview](./README.md) dcastil-tailwind-merge-c8c57aa/docs/features.md000066400000000000000000000156251520007772200216430ustar00rootroot00000000000000# Features ## Merging behavior tailwind-merge is built to be intuitive. It follows a set of rules to determine which class wins when there are conflicts. Here is a brief overview of its conflict resolution. ### Last conflicting class wins ```ts twMerge('p-5 p-2 p-4') // → 'p-4' ``` ### Allows refinements ```ts twMerge('p-3 px-5') // → 'p-3 px-5' twMerge('inset-x-4 right-4') // → 'inset-x-4 right-4' ``` ### Resolves non-trivial conflicts ```ts twMerge('inset-x-px -inset-1') // → '-inset-1' twMerge('bottom-auto inset-y-6') // → 'inset-y-6' twMerge('inline block') // → 'block' ``` ### Supports modifiers and stacked modifiers ```ts twMerge('p-2 hover:p-4') // → 'p-2 hover:p-4' twMerge('hover:p-2 hover:p-4') // → 'hover:p-4' twMerge('hover:focus:p-2 focus:hover:p-4') // → 'focus:hover:p-4' ``` tailwind-merge knows when the order of standard modifiers matters and when not and resolves conflicts accordingly. ### Supports arbitrary values ```ts twMerge('bg-black bg-(--my-color) bg-[color:var(--mystery-var)]') // → 'bg-[color:var(--mystery-var)]' twMerge('grid-cols-[1fr,auto] grid-cols-2') // → 'grid-cols-2' ``` #### Type detection for arbitrary values tailwind-merge automatically detects the type of arbitrary values in most cases, but sometimes explicit labels are needed. **Automatic detection (works out of the box)**: ```ts // Length detected by calc() function twMerge('text-[calc(1rem+2px)] text-lg') // → 'text-lg' // Length detected by unit twMerge('text-[14px] text-[16px]') // → 'text-[16px]' // Color detected by hex format twMerge('text-[#ff0000] text-[#00ff00]') // → 'text-[#00ff00]' // Color detected by color function twMerge('bg-[rgb(255,0,0)] bg-[hsl(0,100%,50%)]') // → 'bg-[hsl(0,100%,50%)]' ``` **When labels are necessary**: For ambiguous classes where tailwind-merge cannot determine the type from context (like `text-*` which can be font-size OR color), you need to use [CSS data type labels](https://tailwindcss.com/docs/adding-custom-styles#resolving-ambiguities): ```ts // ❌ Without label - defaults to color twMerge('text-[theme(myCustomScale.rebecca)] text-lg') // → 'text-[theme(myCustomScale.rebecca)] text-lg' (both kept, treated as different types) // ✅ With label - correctly identified as length/font-size twMerge('text-[length:theme(myCustomScale.rebecca)] text-lg') // → 'text-lg' (conflicting font-size classes) ``` Common labels you might need: - `length:` - for sizes/lengths (font-size, width, etc.) - `color:` - for colors - `position:` - for background-position - `size:` - for background-size - `number:` - for numeric values (font-weight, z-index, etc.) See [Limitations](./limitations.md#you-need-to-use-labels-in-ambiguous-arbitrary-value-classes) for more details on when labels are required. ### Supports arbitrary properties ```ts twMerge('[mask-type:luminance] [mask-type:alpha]') // → '[mask-type:alpha]' twMerge('[--scroll-offset:56px] lg:[--scroll-offset:44px]') // → '[--scroll-offset:56px] lg:[--scroll-offset:44px]' // Don't do this! twMerge('[padding:1rem] p-8') // → '[padding:1rem] p-8' ``` > [!Note] > tailwind-merge does not resolve conflicts between arbitrary properties and their matching Tailwind classes to keep the bundle size small. ### Supports arbitrary variants ```ts twMerge('[&:nth-child(3)]:py-0 [&:nth-child(3)]:py-4') // → '[&:nth-child(3)]:py-4' twMerge('dark:hover:[&:nth-child(3)]:py-0 hover:dark:[&:nth-child(3)]:py-4') // → 'hover:dark:[&:nth-child(3)]:py-4' // Don't do this! twMerge('[&:focus]:ring focus:ring-4') // → '[&:focus]:ring focus:ring-4' ``` > [!Note] > Similarly to arbitrary properties, tailwind-merge does not resolve conflicts between arbitrary variants and their matching predefined modifiers for bundle size reasons. The order of standard modifiers before and after an arbitrary variant in isolation (all modifiers before are one group, all modifiers after are another group) does not matter for tailwind-merge. However, it does matter whether a standard modifier is before or after an arbitrary variant both for Tailwind CSS and tailwind-merge because the resulting CSS selectors are different. ### Supports important modifier ```ts twMerge('p-3! p-4! p-5') // → 'p-4! p-5' twMerge('right-2! -inset-x-1!') // → '-inset-x-1!' ``` ### Supports postfix modifiers ```ts twMerge('text-sm leading-6 text-lg/7') // → 'text-lg/7' ``` ### Preserves non-Tailwind classes ```ts twMerge('p-5 p-2 my-non-tailwind-class p-4') // → 'my-non-tailwind-class p-4' ``` ### Supports custom colors out of the box ```ts twMerge('text-red text-secret-sauce') // → 'text-secret-sauce' ``` ## Composition tailwind-merge has some features that simplify composing class strings together. Those allow you to compose classes like in [clsx](https://www.npmjs.com/package/clsx), [classnames](https://www.npmjs.com/package/classnames) or [classix](https://www.npmjs.com/package/classix). ### Supports multiple arguments ```ts twMerge('some-class', 'another-class yet-another-class', 'so-many-classes') // → 'some-class another-class yet-another-class so-many-classes' ``` ### Supports conditional classes ```ts twMerge('some-class', undefined, null, false, 0) // → 'some-class' twMerge('my-class', false && 'not-this', null && 'also-not-this', true && 'but-this') // → 'my-class but-this' ``` ### Supports arrays and nested arrays ```ts twMerge('some-class', [undefined, ['another-class', false]], ['third-class']) // → 'some-class another-class third-class' twMerge('hi', true && ['hello', ['hey', false]], false && ['bye']) // → 'hi hello hey' ``` Why no object support? [Read here](https://github.com/dcastil/tailwind-merge/discussions/137#discussioncomment-3481605). ## Performance tailwind-merge is optimized for speed when running in the browser. This includes the speed of loading the code and the speed of running the code. ### Results are cached Results get cached by default, so you don't need to worry about wasteful re-renders. The library uses a computationally lightweight [LRU cache]() which stores up to 500 different results by default. The cache is applied after all arguments are [joined](./api-reference.md#twjoin) together to a single string. This means that if you call `twMerge` repeatedly with different arguments that result in the same string when joined, the cache will be hit. The cache size can be modified or opt-out of by using [`extendTailwindMerge`](./api-reference.md#extendtailwindmerge). ### Data structures are reused between calls Expensive computations happen upfront so that `twMerge` calls without a cache hit stay fast. ### Lazy initialization The initial computations are called lazily on the first call to `twMerge` to prevent it from impacting app startup performance if it isn't used initially. --- Next: [Limitations](./limitations.md) Previous: [What is it for](./what-is-it-for.md) [Back to overview](./README.md) dcastil-tailwind-merge-c8c57aa/docs/limitations.md000066400000000000000000000171651520007772200223620ustar00rootroot00000000000000# Limitations tailwind-merge is designed to work intelligently with Tailwind CSS classes, but there are some limitations and edge cases to be aware of. ## Don't use classes that look like Tailwind classes but apply different styles tailwind-merge applies some heuristics to detect the type of a class even if that particular class does not exist in the default Tailwind config. E.g. the class `text-1000xl` does not exist in Tailwind CSS by default but is treated like a `font-size` class in tailwind-merge because it starts with `text-` followed by an optional number and a T-shirt size, like all the other `font-size` classes. This behavior has the advantage that you're less likely to need to configure tailwind-merge if you're only changing or extending some scales in your Tailwind config. But it also means that tailwind-merge treats classes that look like Tailwind classes as Tailwind classes although they might not be defined in your Tailwind config. **Example of potential issues**: ```ts import { twMerge } from 'tailwind-merge' // ❌ Problem: If you create a custom class that matches a Tailwind pattern // Tailwind detects T-shirt sizes with optional numbers: xs, sm, md, lg, xl, 2xl, 3xl, etc. // So even non-existent sizes like text-10xl are treated as font-size classes: twMerge('text-lg text-10xl') // → 'text-10xl' // text-lg is removed because text-10xl matches the font-size pattern (text + number + xl) // This is especially problematic if you want to create custom utility classes // that happen to match these patterns. For example, if you have a custom // `text-2xs` class that applies specific styles (not just font-size): twMerge('text-sm text-2xs') // → 'text-2xs' // text-sm is removed even if your text-2xs does completely different things ``` **How to avoid this**: 1. **Use prefixes that don't match Tailwind patterns** - Instead of `text-2xs`, use `typography-2xs` 2. **Use component-specific prefixes** - Prefix custom utilities with `app-`, `ui-`, or your project name 3. **Don't merge custom component classes** - Keep your custom utility classes separate from classes that are passed through `twMerge` ```ts // ✅ Good: Use non-conflicting naming twMerge('text-lg typography-custom') // → 'text-lg typography-custom' twMerge('text-sm ui-text-special') // → 'text-sm ui-text-special' // ✅ Good: Keep custom classes separate from merging ``` ## You need to use labels in ambiguous arbitrary value classes tailwind-merge detects the type of class by parsing the class name. Some arbitrary value patterns are ambiguous and need explicit labels. ### Arbitrary `font-weight` and `font-family` classes Both `font-weight` and `font-family` use the `font-*` prefix in Tailwind CSS. When using arbitrary variables without labels, tailwind-merge cannot determine whether the class sets font-weight or font-family, so it defaults to treating unlabeled arbitrary variables as font-weight. This matches Tailwind CSS behavior, which also defaults to font-weight for ambiguous `font-*` classes. ```ts // ⚠️ Unlabeled arbitrary variables default to font-weight twMerge('font-(--my-family) font-(--my-weight)') // → 'font-(--my-weight)' (both treated as font-weight, first one removed) // ✅ Use `family-name:` label for font-family twMerge('font-(family-name:--my-family) font-(--my-weight)') // → 'font-(family-name:--my-family) font-(--my-weight)' (both kept) ``` Required labels: - `font-family`: use `family-name:` prefix (e.g., `font-(family-name:--my-font)`) - `font-weight`: no label needed, but you can use `weight:` or `number:` for clarity (e.g., `font-(weight:--my-weight)`) The same applies to arbitrary values with square brackets: ```ts // ⚠️ Unlabeled arbitrary values default to font-weight twMerge('font-[var(--family)] font-[var(--weight)]') // → 'font-[var(--weight)]' // ✅ Use explicit labels twMerge('font-[family-name:var(--family)] font-[var(--weight)]') // → 'font-[family-name:var(--family)] font-[var(--weight)]' ``` ### Arbitrary `background-position` and `background-size` classes When using a class like `bg-[30%_30%]`, tailwind-merge can't determine whether the class is a `background-position` or `background-size` class. ```ts // ❌ Ambiguous - could be position or size // Without a label, tailwind-merge cannot determine the type twMerge('bg-[30%_30%] bg-cover') // → 'bg-[30%_30%] bg-cover' // ✅ Use explicit labels twMerge('bg-[position:30%_30%] bg-cover') // → 'bg-[position:30%_30%] bg-cover' twMerge('bg-[size:30%_30%] bg-cover') // → 'bg-cover' ``` Required labels: - `background-position`: use `position:` prefix (e.g., `bg-[position:30%_30%]`) - `background-size`: use `length:`, `size:`, or `percentage:` prefix (e.g., `bg-[size:200px_100px]`) ### Arbitrary values in `text-*` classes The `text-*` prefix is used for both font-size and text-color. For arbitrary values, tailwind-merge tries to infer the type, but explicit labels help in ambiguous cases: ```ts // ✅ Clear cases (no label needed) twMerge('text-[12px] text-[16px]') // → 'text-[16px]' (detected as font-size) twMerge('text-[#ff0000] text-[#00ff00]') // → 'text-[#00ff00]' (detected as color) // ⚠️ Ambiguous cases (label recommended) twMerge('text-[theme(myCustomScale.value)] text-lg') // → 'text-[theme(myCustomScale.value)] text-lg' (without label, defaults to color interpretation) // ✅ Use explicit label for clarity twMerge('text-[length:theme(myCustomScale.value)] text-lg') // → 'text-lg' ``` ## Arbitrary properties don't merge with standard classes tailwind-merge does not resolve conflicts between arbitrary properties and their matching Tailwind classes to keep bundle size small. ```ts // ❌ These won't conflict with each other twMerge('p-4 [padding:1rem]') // → 'p-4 [padding:1rem]' (both kept) // ✅ Use standard Tailwind classes when possible twMerge('p-4 p-8') // → 'p-8' (correctly merged) ``` **Why this limitation exists**: Parsing arbitrary property names and determining their conflicts would require including CSS property knowledge in the bundle, significantly increasing the library size. ## Arbitrary variants don't merge with standard modifiers Similar to arbitrary properties, arbitrary variants don't conflict with standard modifiers: ```ts // ❌ These won't conflict twMerge('[&:focus]:ring focus:ring-4') // → '[&:focus]:ring focus:ring-4' (both kept) // ✅ Use standard modifiers when possible twMerge('focus:ring focus:ring-4') // → 'focus:ring-4' (correctly merged) ``` ## Doesn't understand custom CSS tailwind-merge only understands Tailwind classes and doesn't analyze your custom CSS: ```ts // Custom classes are preserved but not understood twMerge('my-custom-padding p-4') // → 'my-custom-padding p-4' // Even if my-custom-padding sets padding, tailwind-merge can't know that ``` If you have custom classes created with `@apply` or custom CSS that conflict with Tailwind classes, see the [recipes documentation](./recipes.md#extracting-classes-with-tailwinds-apply) for alternatives. ## Performance with extremely long class strings While tailwind-merge is optimized for typical use cases and includes caching, processing extremely long class strings (1000+ classes) may have performance implications in hot paths: ```ts // ⚠️ Avoid in frequently called render functions if classList is huge const veryLongClassList = Array(1000).fill('p-2 p-3').join(' ') twMerge(veryLongClassList) // Works, but slower with cache misses ``` For most real-world use cases, this is not an issue due to the LRU cache. --- Next: [Configuration](./configuration.md) Previous: [Features](./features.md) [Back to overview](./README.md) dcastil-tailwind-merge-c8c57aa/docs/recipes.md000066400000000000000000000105221520007772200214460ustar00rootroot00000000000000# Recipes How to configure tailwind-merge with some common patterns. ## Adding custom scale from Tailwind config to tailwind-merge config > I have a custom shadow scale with the keys 100, 200 and 300 configured in Tailwind. How do I make tailwind-merge resolve conflicts among those? We'll be able to do this by creating a custom `twMerge` function with [`extendTailwindMerge`](./api-reference.md#extendtailwindmerge). First, we need to know whether we want to override or extend the default scale. Let's say we extended the default config by adding the CSS variable `--shadow-100`, `--shadow-200` and `--shadow-300` into the `@theme` layer, meaning that the default variables like `--shadow-sm` stay the same. Then we check whether our particular theme scale is included in tailwind-merge's theme config object [here](./configuration.md#theme). Because tailwind-merge supports Tailwind's `shadow` theme scale, we can add it to the tailwind-merge config like this: ```js import { extendTailwindMerge } from 'tailwind-merge' const twMerge = extendTailwindMerge({ extend: { theme: { // We only need to define the custom scale values without the `shadow-` prefix when adding them to the theme object shadow: ['100', '200', '300'], }, }, }) ``` In the hypothetical case of the `shadow` theme scale not being supported in tailwind-merge, we would need to check out the [default config of tailwind-merge](../src/lib/default-config.ts) and search for the class group ID of the box shadow scale. After a quick search we would find that tailwind-merge is using the key `shadow` for that group. We could add our custom classes to that group like this: ```js import { extendTailwindMerge } from 'tailwind-merge' const twMerge = extendTailwindMerge({ extend: { classGroups: { // In class groups we always need to define the entire class name like `shadow-100`, `shadow-200` and `shadow-300` // `{ shadow: ['100', '200', '300'] }` is a short-hand syntax for `'shadow-100', 'shadow-200', 'shadow-300'` shadow: [{ shadow: ['100', '200', '300'] }], }, }, }) ``` Note that by using the `extend` object we're only adding our custom classes to the existing ones in the config, so `twMerge('shadow-200 shadow-lg')` will return the string `shadow-lg`. If we want to override the class instead, we need to use the `override` object instead. ## Extracting classes with Tailwind's [`@apply`](https://tailwindcss.com/docs/reusing-styles#extracting-classes-with-apply) > How do I make tailwind-merge resolve conflicts with a custom class created with `@apply`? > > ```css > .btn-primary { > @apply py-2 px-4 bg-blue-500 text-white rounded-lg hover:bg-blue-700; > } > ``` I don't recommend using Tailwind's `@apply` directive for classes that might get processed with tailwind-merge. tailwind-merge would need to be configured so that it knows about which classes `.btn-primary` is in conflict with. This means: If someone adds another Tailwind class to the `@apply` directive, the tailwind-merge config would need to get modified accordingly, keeping it in sync with the written CSS. This easy-to-miss dependency is fragile and can lead to bugs with incorrect merging behavior. Instead of creating custom CSS classes, I recommend keeping the collection of Tailwind classes in a string variable in JavaScript and access it whenever you want to apply those styles. This way you can reuse the collection of styles but don't need to touch the tailwind-merge config. ```jsx // React components with JSX syntax used in this example import { twMerge } from 'tailwind-merge' const BTN_PRIMARY_CLASSNAMES = 'py-2 px-4 bg-blue-500 text-white rounded-lg hover:bg-blue-700' function ButtonPrimary(props) { return ) } function Button({ className, ...props }) { return {/* .bg-red-500 works as expected since it has higher specificity */} ) } function Button({ className, ...props }) { return