From 255b8770c4a8242bde0648396131f416f7916170 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 18 Mar 2025 19:39:17 +0100 Subject: [PATCH 1/5] add failing test --- packages/tailwindcss/src/utilities.test.ts | 32 ++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/packages/tailwindcss/src/utilities.test.ts b/packages/tailwindcss/src/utilities.test.ts index 5b4c6eba94aa..4947cf2759a6 100644 --- a/packages/tailwindcss/src/utilities.test.ts +++ b/packages/tailwindcss/src/utilities.test.ts @@ -17961,6 +17961,38 @@ describe('custom utilities', () => { }" `) }) + + // This originated from a bug. Essentially in the second `--mask-right` we + // call both `--modifier(…)` and `--value(…)`. The moment we processed + // `--modifier(…)` it deleted the `--mask-right` declaration (expected + // behavior). But we didn't properly stop so the `--value(…)` was still + // processed and also tried to remove the `--mask-right` declaration. + // + // This test now ensures that we only remove/replace a declaration once. + test('declaration nodes are only replaced/removed once', async () => { + let input = css` + @utility mask-r-* { + --mask-right: linear-gradient(to left, transparent, black --value(percentage)); + --mask-right: linear-gradient( + to left, + transparent calc(var(--spacing) * --modifier(integer)), + black calc(var(--spacing) * --value(integer)) + ); + mask-image: var(--mask-linear), var(--mask-radial), var(--mask-conic); + } + + @tailwind utilities; + ` + + expect(await compileCss(input, ['mask-r-20%'])).toMatchInlineSnapshot(` + ".mask-r-20\\% { + --mask-right: linear-gradient(to left, transparent, black 20%); + -webkit-mask-image: var(--mask-linear), var(--mask-radial), var(--mask-conic); + -webkit-mask-image: var(--mask-linear), var(--mask-radial), var(--mask-conic); + mask-image: var(--mask-linear), var(--mask-radial), var(--mask-conic); + }" + `) + }) }) test('resolve value based on `@theme`', async () => { From 5d501bc52b9715336ba138d0bb65a0724b1e214b Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 18 Mar 2025 19:38:42 +0100 Subject: [PATCH 2/5] =?UTF-8?q?safety:=20throw=20error=20when=20we=20call?= =?UTF-8?q?=20`replaceNode(=E2=80=A6)`=20multiple=20times?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Other solution is to silently ignore, but that feels even more buggy. --- packages/tailwindcss/src/ast.ts | 3 +++ packages/tailwindcss/src/compat/selector-parser.ts | 3 +++ packages/tailwindcss/src/value-parser.ts | 3 +++ 3 files changed, 9 insertions(+) diff --git a/packages/tailwindcss/src/ast.ts b/packages/tailwindcss/src/ast.ts index 39b6770e25cc..c89127df09c6 100644 --- a/packages/tailwindcss/src/ast.ts +++ b/packages/tailwindcss/src/ast.ts @@ -149,6 +149,9 @@ export function walk( context, path, replaceWith(newNode) { + if (replacedNode) { + throw new Error('Cannot replace a node more than once') + } replacedNode = true if (Array.isArray(newNode)) { diff --git a/packages/tailwindcss/src/compat/selector-parser.ts b/packages/tailwindcss/src/compat/selector-parser.ts index a5ea5e098194..bb54560f90ec 100644 --- a/packages/tailwindcss/src/compat/selector-parser.ts +++ b/packages/tailwindcss/src/compat/selector-parser.ts @@ -98,6 +98,9 @@ export function walk( visit(node, { parent, replaceWith(newNode) { + if (replacedNode) { + throw new Error('Cannot replace a node more than once') + } replacedNode = true if (Array.isArray(newNode)) { diff --git a/packages/tailwindcss/src/value-parser.ts b/packages/tailwindcss/src/value-parser.ts index 07281f95d095..f16e2e0a9314 100644 --- a/packages/tailwindcss/src/value-parser.ts +++ b/packages/tailwindcss/src/value-parser.ts @@ -69,6 +69,9 @@ export function walk( visit(node, { parent, replaceWith(newNode) { + if (replacedNode) { + throw new Error('Cannot replace a node more than once') + } replacedNode = true if (Array.isArray(newNode)) { From 7c104b9c79ee87ee200a8143130ec4bc1d74d1bd Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 18 Mar 2025 19:31:54 +0100 Subject: [PATCH 3/5] stop traversing once we replace the node --- packages/tailwindcss/src/utilities.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tailwindcss/src/utilities.ts b/packages/tailwindcss/src/utilities.ts index 9d8568f686c4..fad65bbf0db3 100644 --- a/packages/tailwindcss/src/utilities.ts +++ b/packages/tailwindcss/src/utilities.ts @@ -4885,7 +4885,7 @@ export function createCssUtility(node: AtRule) { // declaration can be removed. if (modifier === null) { replaceDeclarationWith([]) - return ValueParser.ValueWalkAction.Skip + return ValueParser.ValueWalkAction.Stop } usedModifierFn = true From db4b9e7bf591beabcaeba1bcef229ba66c81587f Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 18 Mar 2025 19:48:26 +0100 Subject: [PATCH 4/5] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 390cd8aed7b3..27ccdd000d5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Pre-process `