@herb-tools/tailwind-class-sorter 0.5.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/LICENSE.txt +22 -0
- package/README.md +253 -0
- package/dist/tailwind-class-sorter.cjs +37113 -0
- package/dist/tailwind-class-sorter.cjs.map +1 -0
- package/dist/tailwind-class-sorter.esm.js +37086 -0
- package/dist/tailwind-class-sorter.esm.js.map +1 -0
- package/dist/types/config.d.ts +2 -0
- package/dist/types/expiring-map.d.ts +6 -0
- package/dist/types/index.d.ts +27 -0
- package/dist/types/resolve.d.ts +4 -0
- package/dist/types/sorter.d.ts +53 -0
- package/dist/types/sorting.d.ts +19 -0
- package/dist/types/types.d.ts +40 -0
- package/package.json +61 -0
- package/src/config.ts +344 -0
- package/src/expiring-map.ts +31 -0
- package/src/index.ts +64 -0
- package/src/resolve.ts +76 -0
- package/src/sorter.ts +106 -0
- package/src/sorting.ts +192 -0
- package/src/types.ts +55 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { SortTailwindClassesOptions } from './types.js';
|
|
2
|
+
export type { SortTailwindClassesOptions, ContextContainer } from './types.js';
|
|
3
|
+
export { TailwindClassSorter } from './sorter.js';
|
|
4
|
+
/**
|
|
5
|
+
* Sort Tailwind CSS classes according to the recommended class order.
|
|
6
|
+
*
|
|
7
|
+
* @param classStr - String of space-separated CSS classes
|
|
8
|
+
* @param options - Configuration options
|
|
9
|
+
* @returns Sorted class string
|
|
10
|
+
*/
|
|
11
|
+
export declare function sortTailwindClasses(classStr: string, options?: SortTailwindClassesOptions): Promise<string>;
|
|
12
|
+
/**
|
|
13
|
+
* Sort a list of Tailwind CSS classes.
|
|
14
|
+
*
|
|
15
|
+
* @param classList - Array of CSS classes
|
|
16
|
+
* @param options - Configuration options
|
|
17
|
+
* @returns Object with sorted classList and removed indices
|
|
18
|
+
*/
|
|
19
|
+
export declare function sortTailwindClassList(classList: string[], options?: SortTailwindClassesOptions): Promise<{
|
|
20
|
+
classList: string[];
|
|
21
|
+
removedIndices: Set<number>;
|
|
22
|
+
} | {
|
|
23
|
+
classList: never;
|
|
24
|
+
removedIndices: Set<unknown>;
|
|
25
|
+
}>;
|
|
26
|
+
export { sortClasses, sortClassList } from './sorting.js';
|
|
27
|
+
export { getTailwindConfig } from './config.js';
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function maybeResolve(name: string): string | null;
|
|
2
|
+
export declare function loadIfExists<T>(name: string): Promise<T | null>;
|
|
3
|
+
export declare function resolveJsFrom(base: string, id: string): string;
|
|
4
|
+
export declare function resolveCssFrom(base: string, id: string): string;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { SortTailwindClassesOptions, ContextContainer } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* A reusable Tailwind CSS class sorter that holds a context for efficient sorting.
|
|
4
|
+
* Use this when you need to sort classes multiple times with the same configuration.
|
|
5
|
+
*/
|
|
6
|
+
export declare class TailwindClassSorter {
|
|
7
|
+
private env;
|
|
8
|
+
/**
|
|
9
|
+
* Create a new TailwindClassSorter with a pre-loaded context.
|
|
10
|
+
*
|
|
11
|
+
* @param contextContainer - Pre-loaded Tailwind context and generateRules function
|
|
12
|
+
* @param options - Configuration options (merged with context)
|
|
13
|
+
*/
|
|
14
|
+
constructor(contextContainer: ContextContainer, options?: SortTailwindClassesOptions);
|
|
15
|
+
/**
|
|
16
|
+
* Create a TailwindClassSorter by loading a Tailwind config file.
|
|
17
|
+
*
|
|
18
|
+
* @param options - Configuration options including config file path
|
|
19
|
+
* @returns Promise resolving to a new TailwindClassSorter instance
|
|
20
|
+
*/
|
|
21
|
+
static fromConfig(options?: SortTailwindClassesOptions): Promise<TailwindClassSorter>;
|
|
22
|
+
/**
|
|
23
|
+
* Sort Tailwind CSS classes synchronously.
|
|
24
|
+
*
|
|
25
|
+
* @param classStr - String of space-separated CSS classes
|
|
26
|
+
* @param overrideOptions - Options to override the instance options for this sort
|
|
27
|
+
* @returns Sorted class string
|
|
28
|
+
*/
|
|
29
|
+
sortClasses(classStr: string, overrideOptions?: Partial<SortTailwindClassesOptions>): string;
|
|
30
|
+
/**
|
|
31
|
+
* Sort a list of Tailwind CSS classes synchronously.
|
|
32
|
+
*
|
|
33
|
+
* @param classList - Array of CSS classes
|
|
34
|
+
* @param overrideOptions - Options to override the instance options for this sort
|
|
35
|
+
* @returns Object with sorted classList and removed indices
|
|
36
|
+
*/
|
|
37
|
+
sortClassList(classList: string[], overrideOptions?: Partial<SortTailwindClassesOptions>): {
|
|
38
|
+
classList: string[];
|
|
39
|
+
removedIndices: Set<number>;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Get the underlying context container.
|
|
43
|
+
*
|
|
44
|
+
* @returns The context container used by this sorter
|
|
45
|
+
*/
|
|
46
|
+
getContext(): ContextContainer;
|
|
47
|
+
/**
|
|
48
|
+
* Get the current options.
|
|
49
|
+
*
|
|
50
|
+
* @returns The options used by this sorter
|
|
51
|
+
*/
|
|
52
|
+
getOptions(): SortTailwindClassesOptions;
|
|
53
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { SortEnv } from './types';
|
|
2
|
+
export declare function bigSign(bigIntValue: bigint): number;
|
|
3
|
+
export declare function sortClasses(classStr: string, { env, ignoreFirst, ignoreLast, removeDuplicates, collapseWhitespace, }: {
|
|
4
|
+
env: SortEnv;
|
|
5
|
+
ignoreFirst?: boolean;
|
|
6
|
+
ignoreLast?: boolean;
|
|
7
|
+
removeDuplicates?: boolean;
|
|
8
|
+
collapseWhitespace?: false | {
|
|
9
|
+
start: boolean;
|
|
10
|
+
end: boolean;
|
|
11
|
+
};
|
|
12
|
+
}): string;
|
|
13
|
+
export declare function sortClassList(classList: string[], { env, removeDuplicates, }: {
|
|
14
|
+
env: SortEnv;
|
|
15
|
+
removeDuplicates: boolean;
|
|
16
|
+
}): {
|
|
17
|
+
classList: string[];
|
|
18
|
+
removedIndices: Set<number>;
|
|
19
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface SortTailwindClassesOptions {
|
|
2
|
+
/**
|
|
3
|
+
* Path to the Tailwind config file.
|
|
4
|
+
*/
|
|
5
|
+
tailwindConfig?: string;
|
|
6
|
+
/**
|
|
7
|
+
* Path to the CSS stylesheet used by Tailwind CSS (v4+)
|
|
8
|
+
*/
|
|
9
|
+
tailwindStylesheet?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Preserve whitespace around Tailwind classes when sorting.
|
|
12
|
+
*/
|
|
13
|
+
tailwindPreserveWhitespace?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Preserve duplicate classes inside a class list when sorting.
|
|
16
|
+
*/
|
|
17
|
+
tailwindPreserveDuplicates?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Base directory for resolving config files (defaults to process.cwd())
|
|
20
|
+
*/
|
|
21
|
+
baseDir?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface TailwindContext {
|
|
24
|
+
tailwindConfig: {
|
|
25
|
+
prefix: string | ((selector: string) => string);
|
|
26
|
+
};
|
|
27
|
+
getClassOrder?: (classList: string[]) => [string, bigint | null][];
|
|
28
|
+
layerOrder: {
|
|
29
|
+
components: bigint;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export interface SortEnv {
|
|
33
|
+
context: TailwindContext;
|
|
34
|
+
generateRules: (classes: Iterable<string>, context: TailwindContext) => [bigint][];
|
|
35
|
+
options: SortTailwindClassesOptions;
|
|
36
|
+
}
|
|
37
|
+
export interface ContextContainer {
|
|
38
|
+
context: any;
|
|
39
|
+
generateRules: (classes: Iterable<string>, context: TailwindContext) => [bigint][];
|
|
40
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "module",
|
|
3
|
+
"name": "@herb-tools/tailwind-class-sorter",
|
|
4
|
+
"description": "Standalone Tailwind CSS class sorter with Prettier plugin compatibility, extracted from tailwindlabs/prettier-plugin-tailwindcss",
|
|
5
|
+
"version": "0.5.0",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "./dist/tailwind-class-sorter.cjs",
|
|
8
|
+
"module": "./dist/tailwind-class-sorter.esm.js",
|
|
9
|
+
"types": "./dist/types/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
"./package.json": "./package.json",
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/types/index.d.ts",
|
|
14
|
+
"import": "./dist/tailwind-class-sorter.esm.js",
|
|
15
|
+
"require": "./dist/tailwind-class-sorter.cjs",
|
|
16
|
+
"default": "./dist/tailwind-class-sorter.esm.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"package.json",
|
|
21
|
+
"README.md",
|
|
22
|
+
"dist/",
|
|
23
|
+
"src/"
|
|
24
|
+
],
|
|
25
|
+
"bugs": "https://github.com/marcoroth/herb/issues/new?title=Package%20%60@herb-tools/tailwind-class-sorter%60:%20",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/marcoroth/herb.git",
|
|
29
|
+
"directory": "javascript/packages/tailwind-class-sorter"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "yarn clean && rollup -c",
|
|
33
|
+
"dev": "rollup -c -w",
|
|
34
|
+
"clean": "rimraf dist",
|
|
35
|
+
"test": "vitest run",
|
|
36
|
+
"test:watch": "vitest --watch",
|
|
37
|
+
"prepublishOnly": "yarn clean && yarn build && yarn test"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"clear-module": "^4.1.2",
|
|
41
|
+
"escalade": "^3.2.0",
|
|
42
|
+
"jiti": "^2.5.1",
|
|
43
|
+
"postcss": "^8.5.6",
|
|
44
|
+
"postcss-import": "^16.1.1"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/node": "^24.1.0",
|
|
48
|
+
"dedent": "^1.6.0",
|
|
49
|
+
"esbuild": "^0.25.8",
|
|
50
|
+
"rimraf": "^6.0.1",
|
|
51
|
+
"tailwindcss": "^3.4.17",
|
|
52
|
+
"tsup": "^8.5.0",
|
|
53
|
+
"vitest": "^3.2.4"
|
|
54
|
+
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"tailwindcss": "^3.0 || ^4.0"
|
|
57
|
+
},
|
|
58
|
+
"engines": {
|
|
59
|
+
"node": ">=14.21.3"
|
|
60
|
+
}
|
|
61
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import * as fs from 'fs/promises'
|
|
3
|
+
import * as path from 'path'
|
|
4
|
+
import { pathToFileURL } from 'url'
|
|
5
|
+
import clearModule from 'clear-module'
|
|
6
|
+
import escalade from 'escalade/sync'
|
|
7
|
+
import { createJiti, type Jiti } from 'jiti'
|
|
8
|
+
import postcss from 'postcss'
|
|
9
|
+
// @ts-ignore
|
|
10
|
+
import postcssImport from 'postcss-import'
|
|
11
|
+
// @ts-ignore
|
|
12
|
+
import { generateRules as generateRulesFallback } from 'tailwindcss/lib/lib/generateRules'
|
|
13
|
+
// @ts-ignore
|
|
14
|
+
import { createContext as createContextFallback } from 'tailwindcss/lib/lib/setupContextUtils'
|
|
15
|
+
import loadConfigFallback from 'tailwindcss/loadConfig'
|
|
16
|
+
import resolveConfigFallback from 'tailwindcss/resolveConfig'
|
|
17
|
+
import type { RequiredConfig } from 'tailwindcss/types/config.js'
|
|
18
|
+
import { expiringMap } from './expiring-map.js'
|
|
19
|
+
import { resolveCssFrom, resolveJsFrom } from './resolve'
|
|
20
|
+
import type { ContextContainer, SortTailwindClassesOptions } from './types'
|
|
21
|
+
|
|
22
|
+
let sourceToPathMap = new Map<string, string | null>()
|
|
23
|
+
let sourceToEntryMap = new Map<string, string | null>()
|
|
24
|
+
let pathToContextMap = expiringMap<string | null, ContextContainer>(10_000)
|
|
25
|
+
|
|
26
|
+
export async function getTailwindConfig(
|
|
27
|
+
options: SortTailwindClassesOptions = {},
|
|
28
|
+
): Promise<ContextContainer> {
|
|
29
|
+
let pkgName = 'tailwindcss'
|
|
30
|
+
|
|
31
|
+
let key = [
|
|
32
|
+
options.baseDir ?? process.cwd(),
|
|
33
|
+
options.tailwindStylesheet ?? '',
|
|
34
|
+
options.tailwindConfig ?? '',
|
|
35
|
+
pkgName,
|
|
36
|
+
].join(':')
|
|
37
|
+
|
|
38
|
+
let baseDir = getBaseDir(options)
|
|
39
|
+
|
|
40
|
+
// Map the source file to it's associated Tailwind config file
|
|
41
|
+
let configPath = sourceToPathMap.get(key)
|
|
42
|
+
if (configPath === undefined) {
|
|
43
|
+
configPath = getConfigPath(options, baseDir)
|
|
44
|
+
sourceToPathMap.set(key, configPath)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let entryPoint = sourceToEntryMap.get(key)
|
|
48
|
+
if (entryPoint === undefined) {
|
|
49
|
+
entryPoint = getEntryPoint(options, baseDir)
|
|
50
|
+
sourceToEntryMap.set(key, entryPoint)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Now see if we've loaded the Tailwind config file before (and it's still valid)
|
|
54
|
+
let contextKey = `${pkgName}:${configPath}:${entryPoint}`
|
|
55
|
+
let existing = pathToContextMap.get(contextKey)
|
|
56
|
+
if (existing) {
|
|
57
|
+
return existing
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// By this point we know we need to load the Tailwind config file
|
|
61
|
+
let result = await loadTailwindConfig(
|
|
62
|
+
baseDir,
|
|
63
|
+
pkgName,
|
|
64
|
+
configPath,
|
|
65
|
+
entryPoint,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
pathToContextMap.set(contextKey, result)
|
|
69
|
+
|
|
70
|
+
return result
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getBaseDir(options: SortTailwindClassesOptions): string {
|
|
74
|
+
if (options.baseDir) {
|
|
75
|
+
return options.baseDir
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return process.cwd()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function loadTailwindConfig(
|
|
82
|
+
baseDir: string,
|
|
83
|
+
pkgName: string,
|
|
84
|
+
tailwindConfigPath: string | null,
|
|
85
|
+
entryPoint: string | null,
|
|
86
|
+
): Promise<ContextContainer> {
|
|
87
|
+
let createContext = createContextFallback
|
|
88
|
+
let generateRules = generateRulesFallback
|
|
89
|
+
let resolveConfig = resolveConfigFallback
|
|
90
|
+
let loadConfig = loadConfigFallback
|
|
91
|
+
let tailwindConfig: RequiredConfig = { content: [] }
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
let pkgFile = resolveJsFrom(baseDir, `${pkgName}/package.json`)
|
|
95
|
+
let pkgDir = path.dirname(pkgFile)
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
let v4 = await loadV4(baseDir, pkgDir, pkgName, entryPoint)
|
|
99
|
+
if (v4) {
|
|
100
|
+
return v4
|
|
101
|
+
}
|
|
102
|
+
} catch {}
|
|
103
|
+
|
|
104
|
+
resolveConfig = require(path.join(pkgDir, 'resolveConfig'))
|
|
105
|
+
createContext = require(
|
|
106
|
+
path.join(pkgDir, 'lib/lib/setupContextUtils'),
|
|
107
|
+
).createContext
|
|
108
|
+
generateRules = require(
|
|
109
|
+
path.join(pkgDir, 'lib/lib/generateRules'),
|
|
110
|
+
).generateRules
|
|
111
|
+
|
|
112
|
+
// Prior to `tailwindcss@3.3.0` this won't exist so we load it last
|
|
113
|
+
loadConfig = require(path.join(pkgDir, 'loadConfig'))
|
|
114
|
+
} catch {}
|
|
115
|
+
|
|
116
|
+
if (tailwindConfigPath) {
|
|
117
|
+
try {
|
|
118
|
+
clearModule(tailwindConfigPath)
|
|
119
|
+
const loadedConfig = loadConfig(tailwindConfigPath)
|
|
120
|
+
tailwindConfig = loadedConfig.default ?? loadedConfig
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.warn(`Failed to load Tailwind config from ${tailwindConfigPath}:`, error)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// suppress "empty content" warning
|
|
127
|
+
tailwindConfig.content = ['no-op']
|
|
128
|
+
|
|
129
|
+
// Create the context
|
|
130
|
+
let context = createContext(resolveConfig(tailwindConfig))
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
context,
|
|
134
|
+
generateRules,
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Create a loader function that can load plugins and config files relative to
|
|
140
|
+
* the CSS file that uses them. However, we don't want missing files to prevent
|
|
141
|
+
* everything from working so we'll let the error handler decide how to proceed.
|
|
142
|
+
*/
|
|
143
|
+
function createLoader<T>({
|
|
144
|
+
legacy,
|
|
145
|
+
jiti,
|
|
146
|
+
filepath,
|
|
147
|
+
onError,
|
|
148
|
+
}: {
|
|
149
|
+
legacy: boolean
|
|
150
|
+
jiti: Jiti
|
|
151
|
+
filepath: string
|
|
152
|
+
onError: (id: string, error: unknown, resourceType: string) => T
|
|
153
|
+
}) {
|
|
154
|
+
let cacheKey = `${+Date.now()}`
|
|
155
|
+
|
|
156
|
+
async function loadFile(id: string, base: string, resourceType: string) {
|
|
157
|
+
try {
|
|
158
|
+
let resolved = resolveJsFrom(base, id)
|
|
159
|
+
|
|
160
|
+
let url = pathToFileURL(resolved)
|
|
161
|
+
url.searchParams.append('t', cacheKey)
|
|
162
|
+
|
|
163
|
+
return await jiti.import(url.href, { default: true })
|
|
164
|
+
} catch (err) {
|
|
165
|
+
return onError(id, err, resourceType)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (legacy) {
|
|
170
|
+
let baseDir = path.dirname(filepath)
|
|
171
|
+
return (id: string) => loadFile(id, baseDir, 'module')
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return async (id: string, base: string, resourceType: string) => {
|
|
175
|
+
return {
|
|
176
|
+
base,
|
|
177
|
+
module: await loadFile(id, base, resourceType),
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function loadV4(
|
|
183
|
+
baseDir: string,
|
|
184
|
+
pkgDir: string,
|
|
185
|
+
pkgName: string,
|
|
186
|
+
entryPoint: string | null,
|
|
187
|
+
) {
|
|
188
|
+
// Import Tailwind — if this is v4 it'll have APIs we can use directly
|
|
189
|
+
let pkgPath = resolveJsFrom(baseDir, pkgName)
|
|
190
|
+
|
|
191
|
+
let tw = await import(pathToFileURL(pkgPath).toString())
|
|
192
|
+
|
|
193
|
+
// This is not Tailwind v4
|
|
194
|
+
if (!tw.__unstable__loadDesignSystem) {
|
|
195
|
+
return null
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// If the user doesn't define an entrypoint then we use the default theme
|
|
199
|
+
entryPoint = entryPoint ?? `${pkgDir}/theme.css`
|
|
200
|
+
|
|
201
|
+
// Create a Jiti instance that can be used to load plugins and config files
|
|
202
|
+
let jiti = createJiti(import.meta.url, {
|
|
203
|
+
moduleCache: false,
|
|
204
|
+
fsCache: false,
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
let importBasePath = path.dirname(entryPoint)
|
|
208
|
+
|
|
209
|
+
// Resolve imports in the entrypoint to a flat CSS tree
|
|
210
|
+
let css = await fs.readFile(entryPoint, 'utf-8')
|
|
211
|
+
|
|
212
|
+
// Determine if the v4 API supports resolving `@import`
|
|
213
|
+
let supportsImports = false
|
|
214
|
+
try {
|
|
215
|
+
await tw.__unstable__loadDesignSystem('@import "./empty";', {
|
|
216
|
+
loadStylesheet: () => {
|
|
217
|
+
supportsImports = true
|
|
218
|
+
return {
|
|
219
|
+
base: importBasePath,
|
|
220
|
+
content: '',
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
})
|
|
224
|
+
} catch {}
|
|
225
|
+
|
|
226
|
+
if (!supportsImports) {
|
|
227
|
+
let resolveImports = postcss([postcssImport()])
|
|
228
|
+
let result = await resolveImports.process(css, { from: entryPoint })
|
|
229
|
+
css = result.css
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Load the design system and set up a compatible context object that is
|
|
233
|
+
// usable by the rest of the plugin
|
|
234
|
+
let design = await tw.__unstable__loadDesignSystem(css, {
|
|
235
|
+
base: importBasePath,
|
|
236
|
+
|
|
237
|
+
// v4.0.0-alpha.25+
|
|
238
|
+
loadModule: createLoader({
|
|
239
|
+
legacy: false,
|
|
240
|
+
jiti,
|
|
241
|
+
filepath: entryPoint,
|
|
242
|
+
onError: (id, err, resourceType) => {
|
|
243
|
+
console.error(`Unable to load ${resourceType}: ${id}`, err)
|
|
244
|
+
|
|
245
|
+
if (resourceType === 'config') {
|
|
246
|
+
return {}
|
|
247
|
+
} else if (resourceType === 'plugin') {
|
|
248
|
+
return () => {}
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
}),
|
|
252
|
+
|
|
253
|
+
loadStylesheet: async (id: string, base: string) => {
|
|
254
|
+
let resolved = resolveCssFrom(base, id)
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
base: path.dirname(resolved),
|
|
258
|
+
content: await fs.readFile(resolved, 'utf-8'),
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
// v4.0.0-alpha.24 and below
|
|
263
|
+
loadPlugin: createLoader({
|
|
264
|
+
legacy: true,
|
|
265
|
+
jiti,
|
|
266
|
+
filepath: entryPoint,
|
|
267
|
+
onError(id, err) {
|
|
268
|
+
console.error(`Unable to load plugin: ${id}`, err)
|
|
269
|
+
|
|
270
|
+
return () => {}
|
|
271
|
+
},
|
|
272
|
+
}),
|
|
273
|
+
|
|
274
|
+
loadConfig: createLoader({
|
|
275
|
+
legacy: true,
|
|
276
|
+
jiti,
|
|
277
|
+
filepath: entryPoint,
|
|
278
|
+
onError(id, err) {
|
|
279
|
+
console.error(`Unable to load config: ${id}`, err)
|
|
280
|
+
|
|
281
|
+
return {}
|
|
282
|
+
},
|
|
283
|
+
}),
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
context: {
|
|
288
|
+
getClassOrder: (classList: string[]) => design.getClassOrder(classList),
|
|
289
|
+
},
|
|
290
|
+
|
|
291
|
+
// Stubs that are not needed for v4
|
|
292
|
+
generateRules: () => [],
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function getConfigPath(options: SortTailwindClassesOptions, baseDir: string): string | null {
|
|
297
|
+
if (options.tailwindConfig) {
|
|
298
|
+
if (options.tailwindConfig.endsWith('.css')) {
|
|
299
|
+
return null
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return path.resolve(baseDir, options.tailwindConfig)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
let configPath: string | void = undefined
|
|
306
|
+
try {
|
|
307
|
+
configPath = escalade(baseDir, (_dir, names) => {
|
|
308
|
+
if (names.includes('tailwind.config.js')) {
|
|
309
|
+
return 'tailwind.config.js'
|
|
310
|
+
}
|
|
311
|
+
if (names.includes('tailwind.config.cjs')) {
|
|
312
|
+
return 'tailwind.config.cjs'
|
|
313
|
+
}
|
|
314
|
+
if (names.includes('tailwind.config.mjs')) {
|
|
315
|
+
return 'tailwind.config.mjs'
|
|
316
|
+
}
|
|
317
|
+
if (names.includes('tailwind.config.ts')) {
|
|
318
|
+
return 'tailwind.config.ts'
|
|
319
|
+
}
|
|
320
|
+
})
|
|
321
|
+
} catch {}
|
|
322
|
+
|
|
323
|
+
if (configPath) {
|
|
324
|
+
return configPath
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return null
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function getEntryPoint(options: SortTailwindClassesOptions, baseDir: string): string | null {
|
|
331
|
+
if (options.tailwindStylesheet) {
|
|
332
|
+
return path.resolve(baseDir, options.tailwindStylesheet)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (options.tailwindConfig && options.tailwindConfig.endsWith('.css')) {
|
|
336
|
+
console.warn(
|
|
337
|
+
'Use the `tailwindStylesheet` option for v4 projects instead of `tailwindConfig`.',
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
return path.resolve(baseDir, options.tailwindConfig)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return null
|
|
344
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
interface ExpiringMap<K, V> {
|
|
2
|
+
get(key: K): V | undefined
|
|
3
|
+
set(key: K, value: V): void
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function expiringMap<K, V>(duration: number): ExpiringMap<K, V> {
|
|
7
|
+
let map = new Map<K, { value: V; expiration: Date }>()
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
get(key: K) {
|
|
11
|
+
let result = map.get(key)
|
|
12
|
+
if (!result) return undefined
|
|
13
|
+
if (result.expiration <= new Date()) {
|
|
14
|
+
map.delete(key)
|
|
15
|
+
return undefined
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return result.value
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
set(key: K, value: V) {
|
|
22
|
+
let expiration = new Date()
|
|
23
|
+
expiration.setMilliseconds(expiration.getMilliseconds() + duration)
|
|
24
|
+
|
|
25
|
+
map.set(key, {
|
|
26
|
+
value,
|
|
27
|
+
expiration,
|
|
28
|
+
})
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { getTailwindConfig } from './config.js'
|
|
2
|
+
import { sortClasses, sortClassList } from './sorting.js'
|
|
3
|
+
import type { SortTailwindClassesOptions, SortEnv } from './types.js'
|
|
4
|
+
|
|
5
|
+
export type { SortTailwindClassesOptions, ContextContainer } from './types.js'
|
|
6
|
+
export { TailwindClassSorter } from './sorter.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Sort Tailwind CSS classes according to the recommended class order.
|
|
10
|
+
*
|
|
11
|
+
* @param classStr - String of space-separated CSS classes
|
|
12
|
+
* @param options - Configuration options
|
|
13
|
+
* @returns Sorted class string
|
|
14
|
+
*/
|
|
15
|
+
export async function sortTailwindClasses(
|
|
16
|
+
classStr: string,
|
|
17
|
+
options: SortTailwindClassesOptions = {}
|
|
18
|
+
): Promise<string> {
|
|
19
|
+
if (!classStr || typeof classStr !== 'string') {
|
|
20
|
+
return classStr
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const { context, generateRules } = await getTailwindConfig(options)
|
|
24
|
+
|
|
25
|
+
const env: SortEnv = {
|
|
26
|
+
context,
|
|
27
|
+
generateRules,
|
|
28
|
+
options
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return sortClasses(classStr, { env })
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Sort a list of Tailwind CSS classes.
|
|
36
|
+
*
|
|
37
|
+
* @param classList - Array of CSS classes
|
|
38
|
+
* @param options - Configuration options
|
|
39
|
+
* @returns Object with sorted classList and removed indices
|
|
40
|
+
*/
|
|
41
|
+
export async function sortTailwindClassList(
|
|
42
|
+
classList: string[],
|
|
43
|
+
options: SortTailwindClassesOptions = {}
|
|
44
|
+
) {
|
|
45
|
+
if (!Array.isArray(classList)) {
|
|
46
|
+
return { classList, removedIndices: new Set() }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const { context, generateRules } = await getTailwindConfig(options)
|
|
50
|
+
|
|
51
|
+
const env: SortEnv = {
|
|
52
|
+
context,
|
|
53
|
+
generateRules,
|
|
54
|
+
options
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return sortClassList(classList, {
|
|
58
|
+
env,
|
|
59
|
+
removeDuplicates: !options.tailwindPreserveDuplicates
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export { sortClasses, sortClassList } from './sorting.js'
|
|
64
|
+
export { getTailwindConfig } from './config.js'
|