diff --git a/CHANGELOG.md b/CHANGELOG.md index 1acd15b57212..96d38c1b1b1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Add missing `main` and `browser` fields for `@tailwindcss/browser` ([#15594](https://github.com/tailwindlabs/tailwindcss/pull/15594)) +- _Upgrade (experimental)_: Pretty print `--spacing(…)` to prevent ambiguity ([#15596](https://github.com/tailwindlabs/tailwindcss/pull/15596)) ## [4.0.0-beta.9] - 2025-01-09 diff --git a/packages/@tailwindcss-upgrade/src/template/codemods/theme-to-var.test.ts b/packages/@tailwindcss-upgrade/src/template/codemods/theme-to-var.test.ts index 2ff0740107f3..c5131fc29499 100644 --- a/packages/@tailwindcss-upgrade/src/template/codemods/theme-to-var.test.ts +++ b/packages/@tailwindcss-upgrade/src/template/codemods/theme-to-var.test.ts @@ -22,6 +22,13 @@ test.each([ ['bg-[theme(colors.red.500)]', 'bg-(--color-red-500)'], // Arbitrary value ['bg-[size:theme(spacing.4)]', 'bg-[size:--spacing(4)]'], // Arbitrary value + data type hint + // Pretty print CSS functions preceded by an operator to prevent consecutive + // operator characters. + ['w-[calc(100dvh-theme(spacing.2))]', 'w-[calc(100dvh-(--spacing(2)))]'], + ['w-[calc(100dvh+theme(spacing.2))]', 'w-[calc(100dvh+(--spacing(2)))]'], + ['w-[calc(100dvh/theme(spacing.2))]', 'w-[calc(100dvh/(--spacing(2)))]'], + ['w-[calc(100dvh*theme(spacing.2))]', 'w-[calc(100dvh*(--spacing(2)))]'], + // Convert to `var(…)` if we can resolve the path, but keep fallback values ['bg-[theme(colors.red.500,red)]', 'bg-(--color-red-500,red)'], diff --git a/packages/@tailwindcss-upgrade/src/template/codemods/theme-to-var.ts b/packages/@tailwindcss-upgrade/src/template/codemods/theme-to-var.ts index 128b5e8b8386..6248d43473fb 100644 --- a/packages/@tailwindcss-upgrade/src/template/codemods/theme-to-var.ts +++ b/packages/@tailwindcss-upgrade/src/template/codemods/theme-to-var.ts @@ -237,7 +237,7 @@ function substituteFunctionsInValue( ast: ValueParser.ValueAstNode[], handle: (value: string, fallback?: string) => string | null, ) { - ValueParser.walk(ast, (node, { replaceWith }) => { + ValueParser.walk(ast, (node, { parent, replaceWith }) => { if (node.kind === 'function' && node.value === 'theme') { if (node.nodes.length < 1) return @@ -275,6 +275,37 @@ function substituteFunctionsInValue( fallbackValues.length > 0 ? handle(path, ValueParser.toCss(fallbackValues)) : handle(path) if (replacement === null) return + if (parent) { + let idx = parent.nodes.indexOf(node) - 1 + while (idx !== -1) { + let previous = parent.nodes[idx] + // Skip the space separator + if (previous.kind === 'separator' && previous.value.trim() === '') { + idx -= 1 + continue + } + + // If the previous node is a word and contains an operator, we need to + // wrap the replacement in parentheses to make the output less + // ambiguous. + // + // Input: + // - `calc(100dvh-theme(spacing.2))` + // + // Output: + // - `calc(100dvh-(--spacing(2)))` + // + // Not: + // -`calc(100dvh---spacing(2))` + // + if (/^[-+*/]$/.test(previous.value.trim())) { + replacement = `(${replacement})` + } + + break + } + } + replaceWith(ValueParser.parse(replacement)) } }) diff --git a/packages/tailwindcss/src/css-functions.test.ts b/packages/tailwindcss/src/css-functions.test.ts index c91f5fee7787..128053f46cbf 100644 --- a/packages/tailwindcss/src/css-functions.test.ts +++ b/packages/tailwindcss/src/css-functions.test.ts @@ -177,7 +177,9 @@ describe('--theme(…)', () => { color: --theme(colors.red.500); } `), - ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: The --theme(…) function can only be used with CSS variables from your theme.]`) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: The --theme(…) function can only be used with CSS variables from your theme.]`, + ) }) })