-
-
Notifications
You must be signed in to change notification settings - Fork 5.1k
Description
What version of Tailwind CSS are you using?
v4.2.2
What build tool (or framework if it abstracts the build tool) are you using?
CLI only (tailwindcss canonicalize --stream)
What version of Node.js are you using?
v24.10.0
What browser are you using?
N/A
What operating system are you using?
macOS (arm64)
Reproduction URL
https://github.com/neilberkman/tailwindcss-stream-repro
Describe your issue
tailwindcss canonicalize --stream produces different output for the same class string depending on what was processed earlier in the stream. The output is non-deterministic with respect to the input line.
Reproduction
# Alone: px/py pair is NOT collapsed to the shorthand
$ echo "px-[1.2rem] py-[1.2rem] text-left" \
| tailwindcss canonicalize --stream
px-[1.2rem] py-[1.2rem] text-left
# After p-[1.2rem] appears earlier in the stream: px/py pair IS collapsed
$ printf "p-[1.2rem] text-sm\npx-[1.2rem] py-[1.2rem] text-left\n" \
| tailwindcss canonicalize --stream
p-[1.2rem] text-sm
p-[1.2rem] text-leftThe second input (px-[1.2rem] py-[1.2rem]) produces different output depending on whether p-[1.2rem] appeared earlier in the same stream session.
Non-stream mode (tailwindcss canonicalize "px-[1.2rem] py-[1.2rem] text-left") also does not collapse, so the collapse only occurs in --stream mode after state has accumulated.
Impact
The --stream mode is designed for editor/formatter integrations that keep a persistent process open. The canonical_tailwind Elixir library uses --stream with a pool of persistent processes. When mix format processes many files through a single stream, earlier class strings affect the output of later ones. This means:
mix formatis not idempotent: a single pass leaves files thatmix format --check-formattedstill flags as needing changes- The result may depend on file processing order
- CI workflows that run
mix format --check-formattedwill fail
Expected behavior
canonicalize --stream should produce identical output for a given class string regardless of what was processed earlier in the same stream session. Either the CLI should collapse px-[1.2rem] py-[1.2rem] to p-[1.2rem] on first encounter, or it should consistently not collapse it.
Likely related
This appears to be the same class of bug fixed in #19675 ("prevent collapse cache pollution across calls"). That fix addressed DefaultMap.get inserting entries during read-only lookups, mutating shared cache state. The --stream mode (added later in #19796) reuses a single designSystem across all lines, so the same cache pollution pattern can recur across stream inputs.
Related: aptinio/canonical_tailwind#1