@herb-tools/tailwind-class-sorter 0.8.9 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/tailwind-class-sorter.cjs +53 -46
- package/dist/tailwind-class-sorter.cjs.map +1 -1
- package/dist/tailwind-class-sorter.esm.js +53 -46
- package/dist/tailwind-class-sorter.esm.js.map +1 -1
- package/package.json +6 -6
- package/src/config.ts +27 -27
- package/src/expiring-map.ts +3 -3
- package/src/resolve.ts +2 -2
- package/src/sorting.ts +11 -10
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@herb-tools/tailwind-class-sorter",
|
|
4
4
|
"description": "Standalone Tailwind CSS class sorter with Prettier plugin compatibility, extracted from tailwindlabs/prettier-plugin-tailwindcss",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.9.0",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"main": "./dist/tailwind-class-sorter.cjs",
|
|
8
8
|
"module": "./dist/tailwind-class-sorter.esm.js",
|
|
@@ -36,20 +36,20 @@
|
|
|
36
36
|
"pretest": "yarn setup-fixtures",
|
|
37
37
|
"test": "vitest run",
|
|
38
38
|
"test:watch": "vitest --watch",
|
|
39
|
-
"prepublishOnly": "yarn clean && yarn build"
|
|
39
|
+
"prepublishOnly": "yarn clean && yarn build && yarn test"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"clear-module": "^4.1.2",
|
|
43
43
|
"escalade": "^3.2.0",
|
|
44
44
|
"jiti": "^2.5.1",
|
|
45
|
-
"postcss": "^8.5.
|
|
45
|
+
"postcss": "^8.5.8",
|
|
46
46
|
"postcss-import": "^16.1.1"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@types/node": "^25.
|
|
49
|
+
"@types/node": "^25.2.2",
|
|
50
50
|
"dedent": "^1.7.0",
|
|
51
|
-
"esbuild": "^0.27.
|
|
52
|
-
"rimraf": "^6.1.
|
|
51
|
+
"esbuild": "^0.27.3",
|
|
52
|
+
"rimraf": "^6.1.3",
|
|
53
53
|
"tailwindcss": "^3.4.17",
|
|
54
54
|
"tsup": "^8.5.1",
|
|
55
55
|
"vitest": "^4.0.0"
|
package/src/config.ts
CHANGED
|
@@ -13,23 +13,23 @@ import { expiringMap } from './expiring-map.js'
|
|
|
13
13
|
import { resolveCssFrom, resolveJsFrom } from './resolve'
|
|
14
14
|
import type { ContextContainer, SortTailwindClassesOptions } from './types'
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
const sourceToPathMap = new Map<string, string | null>()
|
|
17
|
+
const sourceToEntryMap = new Map<string, string | null>()
|
|
18
|
+
const pathToContextMap = expiringMap<string | null, ContextContainer>(10_000)
|
|
19
19
|
|
|
20
20
|
export async function getTailwindConfig(
|
|
21
21
|
options: SortTailwindClassesOptions = {},
|
|
22
22
|
): Promise<ContextContainer> {
|
|
23
|
-
|
|
23
|
+
const pkgName = 'tailwindcss'
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
const key = [
|
|
26
26
|
options.baseDir ?? process.cwd(),
|
|
27
27
|
options.tailwindStylesheet ?? '',
|
|
28
28
|
options.tailwindConfig ?? '',
|
|
29
29
|
pkgName,
|
|
30
30
|
].join(':')
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
const baseDir = getBaseDir(options)
|
|
33
33
|
|
|
34
34
|
// Map the source file to it's associated Tailwind config file
|
|
35
35
|
let configPath = sourceToPathMap.get(key)
|
|
@@ -45,14 +45,14 @@ export async function getTailwindConfig(
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
// Now see if we've loaded the Tailwind config file before (and it's still valid)
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
const contextKey = `${pkgName}:${configPath}:${entryPoint}`
|
|
49
|
+
const existing = pathToContextMap.get(contextKey)
|
|
50
50
|
if (existing) {
|
|
51
51
|
return existing
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
// By this point we know we need to load the Tailwind config file
|
|
55
|
-
|
|
55
|
+
const result = await loadTailwindConfig(
|
|
56
56
|
baseDir,
|
|
57
57
|
pkgName,
|
|
58
58
|
configPath,
|
|
@@ -85,7 +85,7 @@ async function loadTailwindConfig(
|
|
|
85
85
|
let tailwindConfig: RequiredConfig = { content: [] }
|
|
86
86
|
|
|
87
87
|
try {
|
|
88
|
-
|
|
88
|
+
const pkgPath = resolveJsFrom(baseDir, pkgName)
|
|
89
89
|
let pkgJsonPath: string
|
|
90
90
|
|
|
91
91
|
try {
|
|
@@ -115,14 +115,14 @@ async function loadTailwindConfig(
|
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
|
|
118
|
+
const pkgDir = path.dirname(pkgJsonPath)
|
|
119
119
|
|
|
120
120
|
try {
|
|
121
|
-
|
|
121
|
+
const v4 = await loadV4(baseDir, pkgDir, pkgName, entryPoint)
|
|
122
122
|
if (v4) {
|
|
123
123
|
return v4
|
|
124
124
|
}
|
|
125
|
-
} catch (
|
|
125
|
+
} catch (_error) {
|
|
126
126
|
// V4 loading failed, will try v3 below
|
|
127
127
|
}
|
|
128
128
|
|
|
@@ -136,7 +136,7 @@ async function loadTailwindConfig(
|
|
|
136
136
|
|
|
137
137
|
// Prior to `tailwindcss@3.3.0` this won't exist so we load it last
|
|
138
138
|
loadConfig = require(path.join(pkgDir, 'loadConfig'))
|
|
139
|
-
} catch (
|
|
139
|
+
} catch (_error: any) {
|
|
140
140
|
// Tailwind isn't installed or loading failed, will use defaults
|
|
141
141
|
}
|
|
142
142
|
|
|
@@ -159,7 +159,7 @@ async function loadTailwindConfig(
|
|
|
159
159
|
|
|
160
160
|
tailwindConfig.content = ['no-op']
|
|
161
161
|
|
|
162
|
-
|
|
162
|
+
const context = createContext(resolveConfig(tailwindConfig))
|
|
163
163
|
|
|
164
164
|
return {
|
|
165
165
|
context,
|
|
@@ -183,13 +183,13 @@ function createLoader<T>({
|
|
|
183
183
|
filepath: string
|
|
184
184
|
onError: (id: string, error: unknown, resourceType: string) => T
|
|
185
185
|
}) {
|
|
186
|
-
|
|
186
|
+
const cacheKey = `${+Date.now()}`
|
|
187
187
|
|
|
188
188
|
async function loadFile(id: string, base: string, resourceType: string) {
|
|
189
189
|
try {
|
|
190
|
-
|
|
190
|
+
const resolved = resolveJsFrom(base, id)
|
|
191
191
|
|
|
192
|
-
|
|
192
|
+
const url = pathToFileURL(resolved)
|
|
193
193
|
url.searchParams.append('t', cacheKey)
|
|
194
194
|
|
|
195
195
|
return await jiti.import(url.href, { default: true })
|
|
@@ -199,7 +199,7 @@ function createLoader<T>({
|
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
if (legacy) {
|
|
202
|
-
|
|
202
|
+
const baseDir = path.dirname(filepath)
|
|
203
203
|
return (id: string) => loadFile(id, baseDir, 'module')
|
|
204
204
|
}
|
|
205
205
|
|
|
@@ -218,9 +218,9 @@ async function loadV4(
|
|
|
218
218
|
entryPoint: string | null,
|
|
219
219
|
) {
|
|
220
220
|
// Import Tailwind — if this is v4 it'll have APIs we can use directly
|
|
221
|
-
|
|
221
|
+
const pkgPath = resolveJsFrom(baseDir, pkgName)
|
|
222
222
|
|
|
223
|
-
|
|
223
|
+
const tw = await import(pathToFileURL(pkgPath).toString())
|
|
224
224
|
|
|
225
225
|
// This is not Tailwind v4
|
|
226
226
|
if (!tw.__unstable__loadDesignSystem) {
|
|
@@ -231,12 +231,12 @@ async function loadV4(
|
|
|
231
231
|
entryPoint = entryPoint ?? `${pkgDir}/theme.css`
|
|
232
232
|
|
|
233
233
|
// Create a Jiti instance that can be used to load plugins and config files
|
|
234
|
-
|
|
234
|
+
const jiti = createJiti(import.meta.url, {
|
|
235
235
|
moduleCache: false,
|
|
236
236
|
fsCache: false,
|
|
237
237
|
})
|
|
238
238
|
|
|
239
|
-
|
|
239
|
+
const importBasePath = path.dirname(entryPoint)
|
|
240
240
|
|
|
241
241
|
// Resolve imports in the entrypoint to a flat CSS tree
|
|
242
242
|
let css = await fs.readFile(entryPoint, 'utf-8')
|
|
@@ -256,14 +256,14 @@ async function loadV4(
|
|
|
256
256
|
} catch {}
|
|
257
257
|
|
|
258
258
|
if (!supportsImports) {
|
|
259
|
-
|
|
260
|
-
|
|
259
|
+
const resolveImports = postcss([postcssImport()])
|
|
260
|
+
const result = await resolveImports.process(css, { from: entryPoint })
|
|
261
261
|
css = result.css
|
|
262
262
|
}
|
|
263
263
|
|
|
264
264
|
// Load the design system and set up a compatible context object that is
|
|
265
265
|
// usable by the rest of the plugin
|
|
266
|
-
|
|
266
|
+
const design = await tw.__unstable__loadDesignSystem(css, {
|
|
267
267
|
base: importBasePath,
|
|
268
268
|
|
|
269
269
|
// v4.0.0-alpha.25+
|
|
@@ -283,7 +283,7 @@ async function loadV4(
|
|
|
283
283
|
}),
|
|
284
284
|
|
|
285
285
|
loadStylesheet: async (id: string, base: string) => {
|
|
286
|
-
|
|
286
|
+
const resolved = resolveCssFrom(base, id)
|
|
287
287
|
|
|
288
288
|
return {
|
|
289
289
|
base: path.dirname(resolved),
|
package/src/expiring-map.ts
CHANGED
|
@@ -4,11 +4,11 @@ interface ExpiringMap<K, V> {
|
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
export function expiringMap<K, V>(duration: number): ExpiringMap<K, V> {
|
|
7
|
-
|
|
7
|
+
const map = new Map<K, { value: V; expiration: Date }>()
|
|
8
8
|
|
|
9
9
|
return {
|
|
10
10
|
get(key: K) {
|
|
11
|
-
|
|
11
|
+
const result = map.get(key)
|
|
12
12
|
if (!result) return undefined
|
|
13
13
|
if (result.expiration <= new Date()) {
|
|
14
14
|
map.delete(key)
|
|
@@ -19,7 +19,7 @@ export function expiringMap<K, V>(duration: number): ExpiringMap<K, V> {
|
|
|
19
19
|
},
|
|
20
20
|
|
|
21
21
|
set(key: K, value: V) {
|
|
22
|
-
|
|
22
|
+
const expiration = new Date()
|
|
23
23
|
expiration.setMilliseconds(expiration.getMilliseconds() + duration)
|
|
24
24
|
|
|
25
25
|
map.set(key, {
|
package/src/resolve.ts
CHANGED
|
@@ -53,10 +53,10 @@ export function maybeResolve(name: string) {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
export async function loadIfExists<T>(name: string): Promise<T | null> {
|
|
56
|
-
|
|
56
|
+
const modpath = maybeResolve(name)
|
|
57
57
|
|
|
58
58
|
if (modpath) {
|
|
59
|
-
|
|
59
|
+
const mod = await import(name)
|
|
60
60
|
return mod.default ?? mod
|
|
61
61
|
}
|
|
62
62
|
|
package/src/sorting.ts
CHANGED
|
@@ -8,7 +8,7 @@ function prefixCandidate(
|
|
|
8
8
|
context: TailwindContext,
|
|
9
9
|
selector: string,
|
|
10
10
|
): string {
|
|
11
|
-
|
|
11
|
+
const prefix = context.tailwindConfig.prefix
|
|
12
12
|
return typeof prefix === 'function' ? prefix(selector) : prefix + selector
|
|
13
13
|
}
|
|
14
14
|
|
|
@@ -25,14 +25,14 @@ function getClassOrderPolyfill(
|
|
|
25
25
|
// that don't exist on their own. This will result in them "not existing" and
|
|
26
26
|
// sorting could be weird since you still require them in order to make the
|
|
27
27
|
// host utitlies work properly. (Thanks Biology)
|
|
28
|
-
|
|
28
|
+
const parasiteUtilities = new Set([
|
|
29
29
|
prefixCandidate(env.context, 'group'),
|
|
30
30
|
prefixCandidate(env.context, 'peer'),
|
|
31
31
|
])
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
const classNamesWithOrder: [string, bigint | null][] = []
|
|
34
34
|
|
|
35
|
-
for (
|
|
35
|
+
for (const className of classes) {
|
|
36
36
|
let order: bigint | null =
|
|
37
37
|
env
|
|
38
38
|
.generateRules(new Set([className]), env.context)
|
|
@@ -56,7 +56,7 @@ function reorderClasses(classList: string[], { env }: { env: SortEnv }) {
|
|
|
56
56
|
return classList.map(name => [name, null] as [string, bigint | null])
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
const orderedClasses = env.context.getClassOrder
|
|
60
60
|
? env.context.getClassOrder(classList)
|
|
61
61
|
: getClassOrderPolyfill(classList, { env })
|
|
62
62
|
|
|
@@ -109,8 +109,9 @@ export function sortClasses(
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
let result = ''
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
const parts = classStr.split(/([\t\r\f\n ]+)/)
|
|
113
|
+
const classes = parts.filter((_, i) => i % 2 === 0)
|
|
114
|
+
|
|
114
115
|
let whitespace = parts.filter((_, i) => i % 2 !== 0)
|
|
115
116
|
|
|
116
117
|
if (classes[classes.length - 1] === '') {
|
|
@@ -131,7 +132,7 @@ export function sortClasses(
|
|
|
131
132
|
suffix = `${whitespace.pop() ?? ''}${classes.pop() ?? ''}`
|
|
132
133
|
}
|
|
133
134
|
|
|
134
|
-
|
|
135
|
+
const { classList, removedIndices } = sortClassList(classes, {
|
|
135
136
|
env,
|
|
136
137
|
removeDuplicates,
|
|
137
138
|
})
|
|
@@ -173,10 +174,10 @@ export function sortClassList(
|
|
|
173
174
|
removeDuplicates = false
|
|
174
175
|
}
|
|
175
176
|
|
|
176
|
-
|
|
177
|
+
const removedIndices = new Set<number>()
|
|
177
178
|
|
|
178
179
|
if (removeDuplicates) {
|
|
179
|
-
|
|
180
|
+
const seenClasses = new Set<string>()
|
|
180
181
|
|
|
181
182
|
orderedClasses = orderedClasses.filter(([cls, order], index) => {
|
|
182
183
|
if (seenClasses.has(cls)) {
|