Skip to content

sortTailwindcss+preserveWhitespace corrupts template-literal classes when an interpolation is glued to a hyphen (no space) #24464

Description

@em2724

Description

When a className template literal contains a class name built by directly concatenating a static hyphenated prefix with an interpolation (no space between them, e.g. `size-${size}`), sortTailwindcss splits the token at the hyphen and reattaches the interpolation to the following class instead of leaving it in place. This changes the string's runtime value, not just its formatting.

Note on dynamic Tailwind classes: yes, building class names via string concatenation (e.g. `bg-${color}`) goes against Tailwind's own guidance, since the static content scanner can't resolve it to generate CSS. That's a fair criticism of the source code in one of the examples below — but it's orthogonal to this bug. A formatter's contract is to never change a program's runtime behavior, no matter how the input is written. Even already-broken/non-functional input shouldn't be silently rewritten into differently broken output. The fix belongs in the sorter's tokenizer, not in "don't write it that way."

Repro

.oxfmtrc.json:

{
  "sortTailwindcss": {
    "functions": ["clsx", "cn", "cva"]
  }
}

Input:

export const Foo = ({ size }) => {
  return <div className={`rounded size-${size}`}>hi</div>;
};

Run: oxfmt --write Foo.jsx

Expected

Either the class list is left unsorted (since size-${size} can't be statically resolved), or size-${size} is treated as a single atomic token and reordered as a whole:

<div className={`size-${size} rounded`}>hi</div>

Actual

<div className={`size- rounded${size}`}>hi</div>

The formatter decided the token ends right before ${, split there, and glued the trailing ${size} onto the next class (rounded) instead of the one it was originally hyphenated to (size-). At runtime this evaluates to e.g. size- rounded36 instead of rounded size-36.

A second real-world case, where the interpolation sits mid-list with a trailing /10 opacity modifier:

Input:

<div className={`mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-${color}/10`} />

Output:

<div className={`bg- mb-4 flex h-12 w-12 items-center justify-center rounded-full${color}/10`} />

Same defect: bg- and an unrelated class (rounded-full) end up concatenated with the interpolation and the trailing /10.

Notes

  • Reproduces regardless of whether the string is a bare className={...} template literal or wrapped in a listed sort function.
  • Suggested fix: when a class token contains an unresolvable interpolation directly adjacent to static text (no whitespace boundary), treat the whole token as opaque and either skip sorting for that string or move it as a single unit — never split at the interpolation boundary and reassign the tail to a neighboring token.

Environment

  • oxfmt 0.58.0
  • Config: sortTailwindcss enabled with a stylesheet path and functions: ["clsx", "cn", "cva"]

Metadata

Metadata

Assignees

Labels

Type

Fields

Priority

None yet

Effort

None yet

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions