@runtimestudio/tailwind-sort-php 0.2.1 → 0.3.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/README.md +135 -23
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +34 -20
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/init.js +8 -2
- package/dist/php-strings.d.ts +47 -0
- package/dist/php-strings.js +230 -0
- package/dist/sorter.d.ts +3 -3
- package/dist/sorter.js +3 -4
- package/dist/transform.d.ts +5 -1
- package/dist/transform.js +22 -6
- package/package.json +55 -55
- package/src/cli.ts +121 -104
- package/src/html.ts +118 -118
- package/src/index.ts +1 -0
- package/src/init.ts +97 -89
- package/src/islands.ts +155 -155
- package/src/php-strings.ts +260 -0
- package/src/sorter.ts +17 -18
- package/src/transform.ts +98 -78
package/dist/sorter.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type { SortFn } from './transform.ts';
|
|
|
11
11
|
*/
|
|
12
12
|
export interface SorterOptions {
|
|
13
13
|
/**
|
|
14
|
-
* Tailwind v4 CSS entry point
|
|
14
|
+
* Tailwind v4 CSS entry point.
|
|
15
15
|
*/
|
|
16
16
|
stylesheet: string;
|
|
17
17
|
/**
|
|
@@ -20,8 +20,8 @@ export interface SorterOptions {
|
|
|
20
20
|
base?: string;
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
|
-
* Create a `SortFn` backed by the official `prettier-plugin-tailwindcss` sorting engine,
|
|
24
|
-
* the project's Tailwind v4 stylesheet so custom
|
|
23
|
+
* Create a `SortFn` backed by the official `prettier-plugin-tailwindcss` sorting engine,
|
|
24
|
+
* configured with the project's Tailwind v4 stylesheet so custom tokens and classes sort correctly.
|
|
25
25
|
*
|
|
26
26
|
* Requires `prettier-plugin-tailwindcss` >= 0.8 (the `/sorter` entrypoint).
|
|
27
27
|
*
|
package/dist/sorter.js
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* @see transform.ts - consumes the `SortFn` produced here.
|
|
7
7
|
*/
|
|
8
8
|
/**
|
|
9
|
-
* Create a `SortFn` backed by the official `prettier-plugin-tailwindcss` sorting engine,
|
|
10
|
-
* the project's Tailwind v4 stylesheet so custom
|
|
9
|
+
* Create a `SortFn` backed by the official `prettier-plugin-tailwindcss` sorting engine,
|
|
10
|
+
* configured with the project's Tailwind v4 stylesheet so custom tokens and classes sort correctly.
|
|
11
11
|
*
|
|
12
12
|
* Requires `prettier-plugin-tailwindcss` >= 0.8 (the `/sorter` entrypoint).
|
|
13
13
|
*
|
|
@@ -15,8 +15,7 @@
|
|
|
15
15
|
* @returns A synchronous `SortFn` for use with `transform()`.
|
|
16
16
|
*/
|
|
17
17
|
export async function createTailwindSortFn(opts) {
|
|
18
|
-
// Dynamic import so the core package works without the dependency
|
|
19
|
-
// installed (e.g., when only running tests with a mock sorter).
|
|
18
|
+
// Dynamic import so the core package works without the dependency installed.
|
|
20
19
|
const { createSorter } = await import('prettier-plugin-tailwindcss/sorter');
|
|
21
20
|
const sorter = await createSorter({
|
|
22
21
|
base: opts.base ?? process.cwd(),
|
package/dist/transform.d.ts
CHANGED
|
@@ -24,9 +24,13 @@ import { type HtmlScanOptions } from './html.ts';
|
|
|
24
24
|
*/
|
|
25
25
|
export type SortFn = (classes: string[]) => string[];
|
|
26
26
|
/**
|
|
27
|
-
* Combined options for
|
|
27
|
+
* Combined options for all lexer passes.
|
|
28
28
|
*/
|
|
29
29
|
export interface TransformOptions extends IslandOptions, HtmlScanOptions {
|
|
30
|
+
/**
|
|
31
|
+
* Sort classes in PHP string-literal values too. Off by default; see `php-strings.ts` for eligibility.
|
|
32
|
+
*/
|
|
33
|
+
sortPhpStrings?: boolean;
|
|
30
34
|
}
|
|
31
35
|
/**
|
|
32
36
|
* Rewrite all class attribute values in the template source with sorted classes.
|
package/dist/transform.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { findIslands } from "./islands.js";
|
|
20
20
|
import { maskIslands, findClassAttributes } from "./html.js";
|
|
21
|
+
import { findSortablePhpStrings } from "./php-strings.js";
|
|
21
22
|
/**
|
|
22
23
|
* Rewrite all class attribute values in the template source with sorted classes.
|
|
23
24
|
* Everything outside class attribute values is byte-identical in the result; the function is idempotent.
|
|
@@ -35,17 +36,32 @@ export function transform(src, sortFn, opts = {}) {
|
|
|
35
36
|
const islands = findIslands(src, opts);
|
|
36
37
|
const masked = maskIslands(src, islands);
|
|
37
38
|
const attrs = findClassAttributes(masked, opts);
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
// Collect every edit as a {start, end, text} range, then apply them back-to-front so offsets stay valid.
|
|
40
|
+
// HTML class-attribute values live outside islands; PHP string values live inside them, so the two sets of
|
|
41
|
+
// ranges never overlap.
|
|
42
|
+
const edits = [];
|
|
43
|
+
for (const { valueStart, valueEnd } of attrs) {
|
|
42
44
|
const original = src.slice(valueStart, valueEnd);
|
|
43
45
|
const inner = islands.filter((isl) => isl.start >= valueStart && isl.end <= valueEnd);
|
|
44
46
|
const rewritten = rewriteValue(original, valueStart, inner, sortFn);
|
|
45
|
-
if (rewritten !== original)
|
|
46
|
-
|
|
47
|
+
if (rewritten !== original)
|
|
48
|
+
edits.push({ start: valueStart, end: valueEnd, text: rewritten });
|
|
49
|
+
}
|
|
50
|
+
if (opts.sortPhpStrings) {
|
|
51
|
+
for (const { start, end } of findSortablePhpStrings(src, islands)) {
|
|
52
|
+
const original = src.slice(start, end);
|
|
53
|
+
const tokens = original.split(/\s+/).filter(Boolean);
|
|
54
|
+
if (tokens.length < 2)
|
|
55
|
+
continue; // nothing to reorder; leave byte-identical
|
|
56
|
+
const rewritten = sortFn(tokens).join(' ');
|
|
57
|
+
if (rewritten !== original)
|
|
58
|
+
edits.push({ start, end, text: rewritten });
|
|
47
59
|
}
|
|
48
60
|
}
|
|
61
|
+
edits.sort((a, b) => b.start - a.start);
|
|
62
|
+
let out = src;
|
|
63
|
+
for (const e of edits)
|
|
64
|
+
out = out.slice(0, e.start) + e.text + out.slice(e.end);
|
|
49
65
|
return out;
|
|
50
66
|
}
|
|
51
67
|
function rewriteValue(value, base, islands, sortFn) {
|
package/package.json
CHANGED
|
@@ -1,57 +1,57 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
2
|
+
"name": "@runtimestudio/tailwind-sort-php",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Tailwind CSS Class Sorter for PHP",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"class-sorting",
|
|
7
|
+
"formatter",
|
|
8
|
+
"php",
|
|
9
|
+
"prettier",
|
|
10
|
+
"tailwindcss",
|
|
11
|
+
"wordpress"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/runtime-studio-au/tailwind-sort-php#readme",
|
|
14
|
+
"bugs": "https://github.com/runtime-studio-au/tailwind-sort-php/issues",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"author": "Greg Sevastos <greg@runtimestudio.com.au> (https://runtimestudio.com.au)",
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"src"
|
|
20
|
+
],
|
|
21
|
+
"main": "dist/index.js",
|
|
22
|
+
"types": "dist/index.d.ts",
|
|
23
|
+
"type": "module",
|
|
24
|
+
"bin": {
|
|
25
|
+
"tailwind-sort-php": "dist/cli.js"
|
|
26
|
+
},
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/runtime-studio-au/tailwind-sort-php.git"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsc -p tsconfig.build.json",
|
|
33
|
+
"format": "prettier --write .",
|
|
34
|
+
"format:check": "prettier --check .",
|
|
35
|
+
"prepublishOnly": "npm test && npm run build",
|
|
36
|
+
"sort": "bun run src/cli.ts",
|
|
37
|
+
"sort:check": "bun run src/cli.ts --check",
|
|
38
|
+
"test": "node --test \"test/*.test.ts\""
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/bun": "latest",
|
|
42
|
+
"prettier": "^3.8",
|
|
43
|
+
"prettier-plugin-tailwindcss": "^0.8",
|
|
44
|
+
"tailwindcss": "^4",
|
|
45
|
+
"typescript": ">=5.7"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"prettier": ">=3",
|
|
49
|
+
"prettier-plugin-tailwindcss": ">=0.8"
|
|
50
|
+
},
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=22.18"
|
|
53
|
+
},
|
|
54
|
+
"publishConfig": {
|
|
55
|
+
"access": "public"
|
|
56
|
+
}
|
|
57
57
|
}
|
package/src/cli.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* Options:
|
|
10
10
|
* --stylesheet <path> Tailwind v4 CSS entry
|
|
11
11
|
* --attr <name> Extra attribute to sort (repeatable)
|
|
12
|
+
* --php-source <glob> Also sort class strings in PHP declarations in matching files (repeatable)
|
|
12
13
|
* --check Don't write; exit 1 if any file needs sorting
|
|
13
14
|
* --no-short-tags Don't treat bare `<?` as a PHP open tag
|
|
14
15
|
*
|
|
@@ -24,11 +25,12 @@ import { runInit } from './init.ts';
|
|
|
24
25
|
const IGNORE = ['node_modules', 'vendor', 'dist', '.git'];
|
|
25
26
|
|
|
26
27
|
interface Cli {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
globs: string[];
|
|
29
|
+
stylesheet?: string;
|
|
30
|
+
attrs: string[];
|
|
31
|
+
phpSources: string[];
|
|
32
|
+
check: boolean;
|
|
33
|
+
shortTags: boolean;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
/**
|
|
@@ -38,25 +40,27 @@ interface Cli {
|
|
|
38
40
|
* @returns Parsed CLI options with defaults applied.
|
|
39
41
|
*/
|
|
40
42
|
function parseArgs(argv: string[]): Cli {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
43
|
+
const cli: Cli = {
|
|
44
|
+
globs: [],
|
|
45
|
+
attrs: ['class', 'className'],
|
|
46
|
+
phpSources: [],
|
|
47
|
+
check: false,
|
|
48
|
+
shortTags: true,
|
|
49
|
+
};
|
|
50
|
+
for (let i = 0; i < argv.length; i++) {
|
|
51
|
+
const a = argv[i];
|
|
52
|
+
if (a === '--check') cli.check = true;
|
|
53
|
+
else if (a === '--no-short-tags') cli.shortTags = false;
|
|
54
|
+
else if (a === '--stylesheet') cli.stylesheet = argv[++i];
|
|
55
|
+
else if (a === '--attr') cli.attrs.push(argv[++i]);
|
|
56
|
+
else if (a === '--php-source') cli.phpSources.push(argv[++i]);
|
|
57
|
+
else if (a.startsWith('--')) {
|
|
58
|
+
console.error(`Unknown option: ${a}`);
|
|
59
|
+
process.exit(2);
|
|
60
|
+
} else cli.globs.push(a);
|
|
61
|
+
}
|
|
62
|
+
if (cli.globs.length === 0) cli.globs.push('**/*.php');
|
|
63
|
+
return cli;
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
/**
|
|
@@ -65,17 +69,17 @@ function parseArgs(argv: string[]): Cli {
|
|
|
65
69
|
* @param globs Glob patterns relative to `cwd`.
|
|
66
70
|
*/
|
|
67
71
|
async function* scanFiles(globs: string[]): AsyncGenerator<string> {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
72
|
+
if (typeof (globalThis as any).Bun !== 'undefined') {
|
|
73
|
+
const { Glob } = await import('bun');
|
|
74
|
+
for (const pattern of globs) {
|
|
75
|
+
for await (const f of new Glob(pattern).scan('.')) yield f;
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
const { glob } = await import('node:fs/promises');
|
|
79
|
+
for (const pattern of globs) {
|
|
80
|
+
for await (const f of glob(pattern)) yield f as string;
|
|
81
|
+
}
|
|
77
82
|
}
|
|
78
|
-
}
|
|
79
83
|
}
|
|
80
84
|
|
|
81
85
|
/**
|
|
@@ -87,89 +91,102 @@ async function* scanFiles(globs: string[]): AsyncGenerator<string> {
|
|
|
87
91
|
const ignored = (file: string) => IGNORE.some((d) => file.includes(`${d}/`) || file.startsWith(d));
|
|
88
92
|
|
|
89
93
|
/**
|
|
90
|
-
* Best-effort read of the
|
|
91
|
-
*
|
|
92
|
-
* `tailwindAttributes` (merged into the attribute list).
|
|
94
|
+
* Best-effort read of the resolved Prettier config — the shared source of truth with `prettier-plugin-tailwindcss`.
|
|
95
|
+
* Picks up `tailwindStylesheet`, `tailwindAttributes`, and `tailwindPhpSources`.
|
|
93
96
|
*
|
|
94
|
-
* @returns The
|
|
97
|
+
* @returns The found settings, or an empty object if none are available.
|
|
95
98
|
*/
|
|
96
99
|
async function fromPrettierConfig(): Promise<{
|
|
97
|
-
|
|
98
|
-
|
|
100
|
+
stylesheet?: string;
|
|
101
|
+
attributes?: string[];
|
|
102
|
+
phpSources?: string[];
|
|
99
103
|
}> {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
104
|
+
try {
|
|
105
|
+
const prettier = await import('prettier');
|
|
106
|
+
const { dirname, resolve } = await import('node:path');
|
|
107
|
+
const configFile = await prettier.resolveConfigFile();
|
|
108
|
+
if (!configFile) return {};
|
|
109
|
+
const cfg = (await prettier.resolveConfig(configFile)) as Record<string, unknown> | null;
|
|
110
|
+
if (!cfg) return {};
|
|
111
|
+
const out: { stylesheet?: string; attributes?: string[]; phpSources?: string[] } = {};
|
|
112
|
+
if (typeof cfg.tailwindStylesheet === 'string') {
|
|
113
|
+
out.stylesheet = resolve(dirname(configFile), cfg.tailwindStylesheet);
|
|
114
|
+
}
|
|
115
|
+
if (Array.isArray(cfg.tailwindAttributes)) {
|
|
116
|
+
out.attributes = cfg.tailwindAttributes.filter(
|
|
117
|
+
(a): a is string => typeof a === 'string' && !a.startsWith('/'),
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
if (Array.isArray(cfg.tailwindPhpSources)) {
|
|
121
|
+
out.phpSources = cfg.tailwindPhpSources.filter((p): p is string => typeof p === 'string');
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
} catch {
|
|
125
|
+
return {}; // prettier not installed or config unreadable — flags only
|
|
113
126
|
}
|
|
114
|
-
return out;
|
|
115
|
-
} catch {
|
|
116
|
-
return {}; // prettier not installed or config unreadable — flags only
|
|
117
|
-
}
|
|
118
127
|
}
|
|
119
128
|
|
|
120
129
|
async function main() {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
130
|
+
const argv = process.argv.slice(2);
|
|
131
|
+
if (argv[0] === 'init') return runInit(argv.slice(1));
|
|
132
|
+
|
|
133
|
+
const cli = parseArgs(argv);
|
|
134
|
+
|
|
135
|
+
const pc = await fromPrettierConfig();
|
|
136
|
+
const stylesheet = cli.stylesheet ?? pc.stylesheet;
|
|
137
|
+
if (!stylesheet) {
|
|
138
|
+
console.error(
|
|
139
|
+
'No Tailwind stylesheet found. Pass --stylesheet <path> or set ' +
|
|
140
|
+
'`tailwindStylesheet` in your Prettier config.',
|
|
141
|
+
);
|
|
142
|
+
process.exit(2);
|
|
143
|
+
}
|
|
144
|
+
if (pc.attributes) {
|
|
145
|
+
for (const a of pc.attributes) {
|
|
146
|
+
if (!cli.attrs.includes(a)) cli.attrs.push(a);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const sortFn = await createTailwindSortFn({ stylesheet });
|
|
151
|
+
const opts: TransformOptions = {
|
|
152
|
+
attributes: cli.attrs,
|
|
153
|
+
shortOpenTags: cli.shortTags,
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// Pre-scan the opt-in `tailwindPhpSources` globs; a file gets `sortPhpStrings` only if it matches one.
|
|
157
|
+
const phpSources = [...cli.phpSources, ...(pc.phpSources ?? [])];
|
|
158
|
+
const phpSourceFiles = new Set<string>();
|
|
159
|
+
for await (const file of scanFiles(phpSources)) {
|
|
160
|
+
if (!ignored(file)) phpSourceFiles.add(file);
|
|
138
161
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
console.log(`needs sorting: ${file}`);
|
|
160
|
-
} else {
|
|
161
|
-
await writeFile(file, out);
|
|
162
|
-
console.log(`sorted: ${file}`);
|
|
162
|
+
|
|
163
|
+
let scanned = 0;
|
|
164
|
+
let changed = 0;
|
|
165
|
+
const seen = new Set<string>();
|
|
166
|
+
|
|
167
|
+
// Include php-source files so a designated holder is sorted even outside the main globs; `seen` dedupes.
|
|
168
|
+
for await (const file of scanFiles([...cli.globs, ...phpSources])) {
|
|
169
|
+
if (ignored(file) || seen.has(file)) continue;
|
|
170
|
+
seen.add(file);
|
|
171
|
+
scanned++;
|
|
172
|
+
const src = await readFile(file, 'utf8');
|
|
173
|
+
const out = transform(src, sortFn, { ...opts, sortPhpStrings: phpSourceFiles.has(file) });
|
|
174
|
+
if (out !== src) {
|
|
175
|
+
changed++;
|
|
176
|
+
if (cli.check) {
|
|
177
|
+
console.log(`needs sorting: ${file}`);
|
|
178
|
+
} else {
|
|
179
|
+
await writeFile(file, out);
|
|
180
|
+
console.log(`sorted: ${file}`);
|
|
181
|
+
}
|
|
163
182
|
}
|
|
164
|
-
}
|
|
165
183
|
}
|
|
166
|
-
}
|
|
167
184
|
|
|
168
|
-
|
|
169
|
-
|
|
185
|
+
console.log(`${scanned} file(s) scanned, ${changed} ${cli.check ? 'need(s) sorting' : 'updated'}`);
|
|
186
|
+
if (cli.check && changed > 0) process.exit(1);
|
|
170
187
|
}
|
|
171
188
|
|
|
172
189
|
main().catch((err) => {
|
|
173
|
-
|
|
174
|
-
|
|
190
|
+
console.error(err);
|
|
191
|
+
process.exit(1);
|
|
175
192
|
});
|