Skip to content

canonicalize --stream is stateful: output for a class string changes based on previously processed inputs #19835

@neilberkman

Description

@neilberkman

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-left

The 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:

  1. mix format is not idempotent: a single pass leaves files that mix format --check-formatted still flags as needing changes
  2. The result may depend on file processing order
  3. CI workflows that run mix format --check-formatted will 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions