Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,13 @@ If a local configuration file cannot be found the plugin will fallback to the de

By default this plugin sorts classes in the `class` attribute, any framework-specific equivalents like `className`, `:class`, `[ngClass]`, and any Tailwind `@apply` directives.

You can sort additional attributes using the `tailwindAttributes` option, which takes an array of attribute names:
You can extend this behavior to sort classes in any attribute using the following options:

- `tailwindAttributes`: An array of exact attribute names to sort.
- `tailwindAttributesStartWith`: An array of prefixes to match attributes that begin with a certain string.
- `tailwindAttributesEndWith`: An array of suffixes to match attributes that end with a certain string.

#### Example 1

```json5
// .prettierrc
Expand All @@ -91,6 +97,27 @@ function MyButton({ children }) {
}
```

#### Example 2

```json5
// .prettierrc
{
"tailwindAttributesEndWith": ["ClassName"]
}
```

With this configuration, any class found with suffix `ClassName` will be sorted:

```jsx
function MyButton({ children }) {
return (
<button buttonClassName="bg-blue-600 text-white">
{children}
</button>
);
}
```

## Sorting classes in function calls

In addition to sorting classes in attributes, you can also sort classes in strings provided to function calls. This is useful when working with libraries like [clsx](https://github.com/lukeed/clsx) or [cva](https://cva.style/).
Expand Down
70 changes: 43 additions & 27 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ function createParser(
) {
let customizationDefaults: Customizations = {
staticAttrs: new Set(meta.staticAttrs ?? []),
prefixAttrs: new Set(meta.prefixAttrs ?? []),
suffixAttrs: new Set(meta.suffixAttrs ?? []),
dynamicAttrs: new Set(meta.dynamicAttrs ?? []),
functions: new Set(meta.functions ?? []),
}
Expand Down Expand Up @@ -273,13 +275,11 @@ function transformDynamicJsAttribute(attr: any, env: TransformerEnv) {
}

function transformHtml(ast: any, { env, changes }: TransformerContext) {
let { staticAttrs, dynamicAttrs } = env.customizations
let { dynamicAttrs } = env.customizations
let { parser } = env.options

for (let attr of ast.attrs ?? []) {
if (staticAttrs.has(attr.name)) {
attr.value = sortClasses(attr.value, { env })
} else if (dynamicAttrs.has(attr.name)) {
if (dynamicAttrs.has(attr.name)) {
if (!/[`'"]/.test(attr.value)) {
continue
}
Expand All @@ -289,6 +289,8 @@ function transformHtml(ast: any, { env, changes }: TransformerContext) {
} else {
transformDynamicJsAttribute(attr, env)
}
} else if (isSortableAttribute(attr.name, env.customizations)) {
attr.value = sortClasses(attr.value, { env })
}
}

Expand All @@ -298,11 +300,9 @@ function transformHtml(ast: any, { env, changes }: TransformerContext) {
}

function transformGlimmer(ast: any, { env }: TransformerContext) {
let { staticAttrs } = env.customizations

visit(ast, {
AttrNode(attr, _path, meta) {
if (staticAttrs.has(attr.name) && attr.value) {
if (isSortableAttribute(attr.name, env.customizations) && attr.value) {
meta.sortTextNodes = true
}
},
Expand Down Expand Up @@ -354,12 +354,10 @@ function transformGlimmer(ast: any, { env }: TransformerContext) {
}

function transformLiquid(ast: any, { env }: TransformerContext) {
let { staticAttrs } = env.customizations

function isClassAttr(node: { name: string | { type: string; value: string }[] }) {
return Array.isArray(node.name)
? node.name.every((n) => n.type === 'TextNode' && staticAttrs.has(n.value))
: staticAttrs.has(node.name)
? node.name.every((n) => n.type === 'TextNode' && isSortableAttribute(n.value, env.customizations))
: isSortableAttribute(node.name, env.customizations)
}

function hasSurroundingQuotes(str: string) {
Expand Down Expand Up @@ -566,6 +564,22 @@ function sortTemplateLiteral(
return didChange
}

function isSortableAttribute(name: string, customizations: Customizations) {
const { staticAttrs, suffixAttrs, prefixAttrs } = customizations

if (staticAttrs.has(name)) return true

for (const prefix of prefixAttrs) {
if (name.startsWith(prefix)) return true
}

for (const suffix of suffixAttrs) {
if (name.endsWith(suffix)) return true
}

return false
}

function isSortableTemplateExpression(
node: import('@babel/types').TaggedTemplateExpression | import('ast-types').namedTypes.TaggedTemplateExpression,
functions: Set<string>,
Expand Down Expand Up @@ -653,7 +667,7 @@ function canCollapseWhitespaceIn(path: Path<import('@babel/types').Node, any>) {
// We cross several parsers that share roughly the same shape so things are
// good enough. The actual AST we should be using is probably estree + ts.
function transformJavaScript(ast: import('@babel/types').Node, { env }: TransformerContext) {
let { staticAttrs, functions } = env.customizations
let { functions } = env.customizations

function sortInside(ast: import('@babel/types').Node) {
visit(ast, (node, path) => {
Expand Down Expand Up @@ -685,7 +699,7 @@ function transformJavaScript(ast: import('@babel/types').Node, { env }: Transfor
return
}

if (!staticAttrs.has(node.name.name)) {
if (!isSortableAttribute(node.name.name, env.customizations)) {
return
}

Expand Down Expand Up @@ -787,11 +801,11 @@ function transformCss(ast: any, { env }: TransformerContext) {
}

function transformAstro(ast: any, { env, changes }: TransformerContext) {
let { staticAttrs, dynamicAttrs } = env.customizations
let { dynamicAttrs } = env.customizations

if (ast.type === 'element' || ast.type === 'custom-element' || ast.type === 'component') {
for (let attr of ast.attributes ?? []) {
if (staticAttrs.has(attr.name) && attr.type === 'attribute' && attr.kind === 'quoted') {
if (isSortableAttribute(attr.name, env.customizations) && attr.type === 'attribute' && attr.kind === 'quoted') {
attr.value = sortClasses(attr.value, {
env,
})
Expand All @@ -812,8 +826,6 @@ function transformAstro(ast: any, { env, changes }: TransformerContext) {
}

function transformMarko(ast: any, { env }: TransformerContext) {
let { staticAttrs } = env.customizations

const nodesToVisit = [ast]
while (nodesToVisit.length > 0) {
const currentNode = nodesToVisit.pop()
Expand All @@ -832,7 +844,7 @@ function transformMarko(ast: any, { env }: TransformerContext) {
nodesToVisit.push(...currentNode.body)
break
case 'MarkoAttribute':
if (!staticAttrs.has(currentNode.name)) break
if (!isSortableAttribute(currentNode.name, env.customizations)) break
switch (currentNode.value.type) {
case 'ArrayExpression':
const classList = currentNode.value.elements
Expand All @@ -854,15 +866,13 @@ function transformMarko(ast: any, { env }: TransformerContext) {
}

function transformTwig(ast: any, { env, changes }: TransformerContext) {
let { staticAttrs } = env.customizations

for (let child of ast.expressions ?? []) {
transformTwig(child, { env, changes })
}

visit(ast, {
Attribute(node, _path, meta) {
if (!staticAttrs.has(node.name.name)) return
if (!isSortableAttribute(node.name.name, env.customizations)) return

meta.sortTextNodes = true
},
Expand Down Expand Up @@ -893,16 +903,14 @@ function transformTwig(ast: any, { env, changes }: TransformerContext) {
}

function transformPug(ast: any, { env }: TransformerContext) {
let { staticAttrs } = env.customizations

// This isn't optimal
// We should merge the classes together across class attributes and class tokens
// And then we sort them
// But this is good enough for now

// First sort the classes in attributes
for (const token of ast.tokens) {
if (token.type === 'attribute' && staticAttrs.has(token.name)) {
if (token.type === 'attribute' && isSortableAttribute(token.name, env.customizations)) {
token.val = [token.val.slice(0, 1), sortClasses(token.val.slice(1, -1), { env }), token.val.slice(-1)].join('')
}
}
Expand Down Expand Up @@ -947,10 +955,8 @@ function transformPug(ast: any, { env }: TransformerContext) {
}

function transformSvelte(ast: any, { env, changes }: TransformerContext) {
let { staticAttrs } = env.customizations

for (let attr of ast.attributes ?? []) {
if (!staticAttrs.has(attr.name) || attr.type !== 'Attribute') {
if (!isSortableAttribute(attr.name, env.customizations) || attr.type !== 'Attribute') {
continue
}

Expand Down Expand Up @@ -1238,6 +1244,16 @@ export interface PluginOptions {
*/
tailwindAttributes?: string[]

/**
* List of prefixes to match attributes that contain classes.
*/
tailwindAttributesStartWith?: string[]

/**
* List of suffixes to match attributes that contain classes.
*/
tailwindAttributesEndWith?: string[]

/**
* Preserve whitespace around Tailwind classes when sorting.
*/
Expand Down
28 changes: 28 additions & 0 deletions src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,22 @@ export const options: Record<string, SupportOption> = {
description: 'List of attributes/props that contain sortable Tailwind classes',
},

tailwindAttributesStartWith: {
type: 'string',
array: true,
default: [{ value: [] }],
category: 'Tailwind CSS',
description: 'List of prefixes for attributes that contain sortable Tailwind classes',
},

tailwindAttributesEndWith: {
type: 'string',
array: true,
default: [{ value: [] }],
category: 'Tailwind CSS',
description: 'List of suffixes for attributes that contain sortable Tailwind classes',
},

tailwindFunctions: {
type: 'string',
array: true,
Expand Down Expand Up @@ -63,6 +79,8 @@ export const options: Record<string, SupportOption> = {

export function getCustomizations(options: RequiredOptions, parser: string, defaults: Customizations): Customizations {
let staticAttrs = new Set<string>(defaults.staticAttrs)
let prefixAttrs = new Set<string>(defaults.prefixAttrs)
let suffixAttrs = new Set<string>(defaults.suffixAttrs)
let dynamicAttrs = new Set<string>(defaults.dynamicAttrs)
let functions = new Set<string>(defaults.functions)

Expand Down Expand Up @@ -96,9 +114,19 @@ export function getCustomizations(options: RequiredOptions, parser: string, defa
functions.add(fn)
}

for (let attr of options.tailwindAttributesStartWith ?? []) {
prefixAttrs.add(attr)
}

for (let attr of options.tailwindAttributesEndWith ?? []) {
suffixAttrs.add(attr)
}

return {
functions,
staticAttrs,
dynamicAttrs,
prefixAttrs,
suffixAttrs,
}
}
4 changes: 4 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@ export interface TransformerMetadata {
// Default customizations for a given transformer
functions?: string[]
staticAttrs?: string[]
prefixAttrs?: string[]
suffixAttrs?: string[]
dynamicAttrs?: string[]
}

export interface Customizations {
functions: Set<string>
staticAttrs: Set<string>
prefixAttrs: Set<string>
suffixAttrs: Set<string>
dynamicAttrs: Set<string>
}

Expand Down