feat: tailwindClassOrder — priority-based class bucketing#5
Merged
Conversation
Extract duplicated sort callback logic from sortClassList and sortGroup into shared compareTailwindEntries function. No behavior change. Prepares ground for upcoming bucket-based class ordering (tailwindClassOrder).
Introduce src/class-order.ts with greedy bucket-by-bucket algorithm for
configurable class ordering. Pattern and unknown buckets preserve input
order; only the tailwind bucket sorts (using a caller-supplied comparator).
Config parser accepts flat array or tuple [items, {unspecified}] form,
plus an alias 'tailwindcss' for 'tailwind'. Resolver dispatches on value
type (array | string path | undefined) with jiti-based loading identical
to property-order loader.
Not wired into sortClassList/sortGroup yet — that's Phase C.
Sorting now always runs through applyBuckets from src/class-order.ts.
When the option is not set, a default context ({unknown, tailwind},
unspecified:'top') replicates previous unknown-first + tailwind-by-bigint
behavior — single code-path, no legacy branch.
- src/options.ts: new SupportOption 'tailwindClassOrder' (type:string,
exception:Array.isArray). The exception passthrough lets users put the
config array directly in .prettierrc.* without JSON-string escaping,
while string paths remain supported for external JS/JSON configs.
- src/types.ts: LatteOptions.tailwindClassOrder; TailwindContext.classOrder
(required — always present via defaultClassOrderContext()).
- src/tailwind.ts: loadTailwindContext accepts classOrderRaw; cache key
now includes JSON.stringify(classOrderRaw ?? '') to prevent cross-file
context reuse with different configs.
- src/index.ts: propagate options.tailwindClassOrder into loader.
- src/sorting.ts + src/nclass.ts: sortClassList and sortGroup delegate to
applyBuckets. Dedup runs AFTER bucketing (first-occurrence-wins).
compareTailwindEntries is called only inside the 'tailwind' bucket where
non-null bigint is guaranteed.
Placeholder strings ('...' / '…') now fall into the unknown bucket (stable
input order) instead of being forced to the end. Plan explicitly permits
this — placeholders don't occur in Latte templates (dynamic values go
through n:class token barriers), and the removed special-case matched
prettier-plugin-tailwindcss behavior that isn't meaningful here.
16 test cases across 7 categories: property order interaction, realistic config, n:class parity, greedy stealing, unspecified top/bottom, default context equivalence, BEM scenario. Also documents the 'unknown before pattern' edge case explicitly — if unknown bucket comes first, it consumes all null-bigint classes before a later regex pattern gets a chance to match them.
Adds options table row in README and full reference with use-case examples (JS hooks last, BEM grouping, component-like utilities first) and notes on interaction with tailwindPropertyOrder.
BREAKING CHANGE: Rework matching semantics and config value type.
**Matching model — priority-based (two phases):**
1. Assign each class to a bucket:
- Explicit {pattern} buckets always win, regardless of position in config.
- If no pattern matched: first 'tailwind' (non-null bigint) or first
'unknown' (null bigint). Otherwise unspecified.
2. Emit in config order. 'tailwind' bucket sorts; patterns + 'unknown' stay
in input order. Unspecified leftovers go front/back per 'unspecified'.
This fixes a real-world problem: when a class like 'ajax' is a known TW
utility (non-null bigint), the previous greedy algorithm would let
'tailwind' swallow it before the later {pattern:'^ajax'} bucket had a
chance to match. Priority-first matching removes that ordering trap —
patterns consistently win. Config bucket order now only controls where
each group appears in the output.
**Config value — string only:**
Prettier CLI silently collapses non-'array:true' option arrays to their
last element in .prettierrc, which made the 'exception: Array.isArray'
passthrough trick (spike-tested via API only) unreliable at the config
boundary. Drop it. Option is now type:'string' with two forms:
- path to external JS/JSON file (recommended), or
- JSON-encoded string starting with '['.
Direct array in options still works for API callers (prettier.format),
but is no longer the documented or primary path.
**Tests:** existing greedy-steal scenarios reworded to priority
semantics; added JSON-string resolver coverage and a fokus-optik-style
test where 'ajax' has a non-null bigint and must still land in the
{pattern:'^ajax'} bucket.
sortBranch used to fall through to a "bare multi-word" case that passed anything with whitespace and no leading $ to sortClasses. A ternary branch like "'foo bar' . \->m()" starts with apostrophe (so it failed the strict quoted-string check — no closing apostrophe at the end) but did contain whitespace, so it got split into class tokens and reordered, corrupting the PHP syntax (the ". \->m()" suffix landed in the middle of the class list, breaking the template). Treat any branch that isn't a plain single-quoted string as atomic and leave it untouched. Classes inside quoted fragments of a concatenation could theoretically still be sorted separately, but that's not worth the risk of corrupting the expression. Regression test in tests/nclass.test.ts covers a realistic case from the fokus-optik pdforms-ajax-spinner wrapper.
…arriers
Previously normalize-barriers collapsed every separator between two sortable
tokens to ', ' — including when the user had written each token on its own
line. That erased meaningful multi-line layout like:
n:class="'product-list',
...
'xs:gap-sm',
'sm:[--max-column-count:3]',
..."
where each breakpoint lived on its own line. After sorting the two sortable
neighbours the newline between them vanished and they ended up glued on one
line.
Keep the behaviour intent (normalise horizontal whitespace around the
comma) but preserve the original newline + indentation when the separator
between two sortable tokens contains a \n. Single-line inputs still
collapse to a single ', ' separator.
Updates two whitespace-mode tests that asserted the old collapse behaviour,
adds a new test for the single-line normalisation branch, updates
docs/options.md.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
tailwindClassOrderoption — configurable class ordering through buckets (unknown / tailwind / regex patterns).{pattern}buckets always win over"unknown"/"tailwind"catchalls, regardless of position. Config bucket order controls only where each group appears in the output.n:classparser fixes discovered while testing on a real Latte project.Config shape
Value is a string — path to JS/JSON file, or JSON-encoded array. Direct inline array in
.prettierrcdoesn't work because Prettier CLI collapses non-array:trueoption arrays to their last element.When the option is unset, behaviour is equivalent to the implicit default
[["unknown", "tailwind"], {"unspecified":"top"}]— same output as before.Use cases
{ pattern: '^js-' }as last bucket{ pattern: '^icon(?:--|$)' }{ pattern: '^h[1-6]$' }Breaking changes (relative to unreleased state of the feature)
.prettierrcare not supported — use an external.js/.jsonfile or a JSON-encoded string. (API callers passing arrays toprettier.formatstill work.)Commits
refactor— extractcompareTailwindEntriesshared bysortClassListandsortGroupfeat— newsrc/class-order.ts(types, parser, resolver,applyBuckets)feat— wiretailwindClassOrderthrough options / types / loader / sort pipelinetest— integration tests for bucket algorithm + property-order interaction + BEM + n:class paritydocs— README +docs/options.mdfeat!— priority-based matching, dropexceptionparameter (string-only config)fix— don't split conditional branches that contain PHP concatenation ('foo bar' . $x->m()used to be shredded into tokens and corrupted)fix— preserve newlines between sortable tokens innormalize-barriersmode (previously collapsed multi-linen:classlayout to a single line)Test plan
applyBucketsunspecifiedplacement, n:class parity, property-order interaction--max-warnings=0), build OK (CJS + ESM + DTS)^ajax/^js-/^icon(?:--|$)/^h[1-6]$, default context preserves legacy behaviourn:classfixes