@runtimestudio/tailwind-sort-php 0.2.1 → 0.4.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 +1 -1
- package/README.md +140 -23
- package/dist/cli.d.ts +4 -13
- package/dist/cli.js +84 -41
- package/dist/html.js +3 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/init.js +8 -2
- package/dist/islands.js +7 -94
- package/dist/php-lexer.d.ts +29 -0
- package/dist/php-lexer.js +96 -0
- package/dist/php-strings.d.ts +47 -0
- package/dist/php-strings.js +156 -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 +48 -11
- package/package.json +55 -55
- package/src/cli.ts +168 -118
- package/src/html.ts +119 -118
- package/src/index.ts +1 -0
- package/src/init.ts +97 -89
- package/src/islands.ts +102 -185
- package/src/php-lexer.ts +93 -0
- package/src/php-strings.ts +189 -0
- package/src/sorter.ts +17 -18
- package/src/transform.ts +122 -77
package/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2026
|
|
3
|
+
Copyright (c) 2026 G&J Sevastos Family Trust t/a Runtime Studio
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
package/README.md
CHANGED
|
@@ -18,6 +18,15 @@ values, using a real PHP-aware lexer, and leaves everything else byte-identical.
|
|
|
18
18
|
- **Zero-config** — reads `tailwindStylesheet` from your Prettier config, the same source of truth that
|
|
19
19
|
`prettier-plugin-tailwindcss` uses.
|
|
20
20
|
|
|
21
|
+
## Contents
|
|
22
|
+
|
|
23
|
+
- [Requirements](#requirements) · [Install](#install) · [Setup](#setup) · [Usage](#usage)
|
|
24
|
+
- [Editor integration](#editor-integration) — sort-on-save & pre-commit gate
|
|
25
|
+
- [Sorting classes in PHP declarations](#sorting-classes-in-php-declarations) — the per-file opt-in
|
|
26
|
+
- [WordPress themes & plugins](#wordpress-themes--plugins)
|
|
27
|
+
- [How it handles mixed templates](#how-it-handles-mixed-templates) · [Programmatic API](#programmatic-api) · [Known limitations](#known-limitations)
|
|
28
|
+
- [Development](#development) · [License](#license)
|
|
29
|
+
|
|
21
30
|
## Requirements
|
|
22
31
|
|
|
23
32
|
- **Node ≥ 22.18**, or **Bun** — both run the CLI and the programmatic API.
|
|
@@ -44,8 +53,8 @@ vocabulary. Any config format Prettier supports works (`.prettierrc`, `prettier.
|
|
|
44
53
|
|
|
45
54
|
```js
|
|
46
55
|
export default {
|
|
47
|
-
|
|
48
|
-
|
|
56
|
+
plugins: ['prettier-plugin-tailwindcss'],
|
|
57
|
+
tailwindStylesheet: './resources/css/main.css',
|
|
49
58
|
};
|
|
50
59
|
```
|
|
51
60
|
|
|
@@ -75,18 +84,24 @@ npx tailwind-sort-php init
|
|
|
75
84
|
|
|
76
85
|
### Options
|
|
77
86
|
|
|
78
|
-
| Flag | Description
|
|
79
|
-
|
|
80
|
-
| `--stylesheet <path>` | Tailwind v4 CSS entry. Defaults to `tailwindStylesheet` from your Prettier config.
|
|
81
|
-
| `--attr <name>` | Extra attribute to sort (repeatable). Merged with `tailwindAttributes` from your Prettier config.
|
|
82
|
-
| `--
|
|
83
|
-
| `--
|
|
87
|
+
| Flag | Description |
|
|
88
|
+
|-----------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
89
|
+
| `--stylesheet <path>` | Tailwind v4 CSS entry. Defaults to `tailwindStylesheet` from your Prettier config. |
|
|
90
|
+
| `--attr <name>` | Extra attribute to sort (repeatable). Merged with `tailwindAttributes` from your Prettier config. |
|
|
91
|
+
| `--php-source <glob>` | Also sort class strings in PHP declarations in matching files (repeatable). Merged with `tailwindPhpSources`. See [Sorting classes in PHP declarations](#sorting-classes-in-php-declarations). |
|
|
92
|
+
| `--check` | Don't write; exit 1 if any file needs sorting. |
|
|
93
|
+
| `--no-short-tags` | Don't treat bare `<?` as a PHP open tag. |
|
|
94
|
+
| `-h, --help` | Show usage. |
|
|
95
|
+
| `--version` | Print the version. |
|
|
84
96
|
|
|
85
97
|
Default globs are all `.php` files under the cwd; `node_modules`, `vendor`, `dist`, and `.git` are always skipped.
|
|
86
98
|
|
|
87
99
|
## Editor integration
|
|
88
100
|
|
|
89
|
-
No IDE plugin is needed — two small setups cover the common workflows.
|
|
101
|
+
No IDE plugin is needed — two small setups cover the common workflows: sort-on-save and a pre-commit gate.
|
|
102
|
+
|
|
103
|
+
<details>
|
|
104
|
+
<summary><b>Sort on save</b> (PhpStorm / IntelliJ, VS Code) and a <b>pre-commit gate</b> — click to expand</summary>
|
|
90
105
|
|
|
91
106
|
### Sort on save (PhpStorm / IntelliJ)
|
|
92
107
|
|
|
@@ -107,14 +122,14 @@ to `.vscode/settings.json`:
|
|
|
107
122
|
|
|
108
123
|
```json
|
|
109
124
|
{
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
125
|
+
"emeraldwalk.runonsave": {
|
|
126
|
+
"commands": [
|
|
127
|
+
{
|
|
128
|
+
"match": "\\.php$",
|
|
129
|
+
"cmd": "${workspaceFolder}/node_modules/.bin/tailwind-sort-php ${relativeFile}"
|
|
130
|
+
}
|
|
131
|
+
]
|
|
132
|
+
}
|
|
118
133
|
}
|
|
119
134
|
```
|
|
120
135
|
|
|
@@ -146,11 +161,103 @@ git diff --cached --name-only -z --diff-filter=ACMR -- '*.php' | xargs -0 ./node
|
|
|
146
161
|
|
|
147
162
|
In CI there's no staged diff — just sweep the whole project with `npx tailwind-sort-php --check`.
|
|
148
163
|
|
|
164
|
+
</details>
|
|
165
|
+
|
|
166
|
+
## Sorting classes in PHP declarations
|
|
167
|
+
|
|
168
|
+
By default, the tool only sorts classes inside HTML `class="..."` attributes. Class strings declared in **PHP itself** —
|
|
169
|
+
constants, static properties, config arrays — sit inside PHP code the tool treats as opaque, so they're left alone.
|
|
170
|
+
|
|
171
|
+
Opt in **per file** by listing the files whose PHP string values are Tailwind class lists, via `tailwindPhpSources` in
|
|
172
|
+
your Prettier config (or the repeatable `--php-source <glob>` flag):
|
|
173
|
+
|
|
174
|
+
```js
|
|
175
|
+
export default {
|
|
176
|
+
plugins: ['prettier-plugin-tailwindcss'],
|
|
177
|
+
tailwindStylesheet: './resources/css/main.css',
|
|
178
|
+
tailwindPhpSources: ['src/classes/*.php'],
|
|
179
|
+
};
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
With this set, every **string value** in a matched file is sorted with the exact same engine and order as the HTML
|
|
183
|
+
side. In `key => value` arrays only the **value** is sorted — keys are never touched:
|
|
184
|
+
|
|
185
|
+
```php
|
|
186
|
+
// before
|
|
187
|
+
public const array VARIANTS = array(
|
|
188
|
+
'primary' => 'text-white px-4 bg-blue-600 rounded py-2',
|
|
189
|
+
'secondary' => 'text-gray-900 px-4 bg-gray-100 rounded py-2',
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// after
|
|
193
|
+
public const array VARIANTS = array(
|
|
194
|
+
'primary' => 'rounded bg-blue-600 px-4 py-2 text-white',
|
|
195
|
+
'secondary' => 'rounded bg-gray-100 px-4 py-2 text-gray-900',
|
|
196
|
+
);
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Scalar declarations work the same way (`const string CARD = '...'`, `static $x = '...'`, `$x = '...'`), as do nested
|
|
200
|
+
and list-style arrays.
|
|
201
|
+
|
|
202
|
+
> **⚠️ Point `tailwindPhpSources` only at files whose string values are all Tailwind class lists.** The tool does
|
|
203
|
+
> **not** guess whether a string "looks like" classes — within a matched file it sorts **every** eligible string value.
|
|
204
|
+
> Aimed at a general file, it **will** reorder the words inside non-class strings (labels, URLs, SQL). This is a
|
|
205
|
+
> deliberate design contract, not a bug: safety comes from your file-level opt-in, which is why a dedicated
|
|
206
|
+
> directory of class-holder files (e.g. `src/classes/`) is the intended target.
|
|
207
|
+
|
|
208
|
+
The opt-in is **inert at runtime** — it lives in formatter config only, never in your source. Your PHP stays vanilla,
|
|
209
|
+
with zero coupling to this tool (no marker comments, no helper functions, no attributes).
|
|
210
|
+
|
|
211
|
+
**Skipped automatically** (left byte-identical, even in a matched file):
|
|
212
|
+
|
|
213
|
+
- Concatenated literals (`'btn-' . $variant`) — a fragment joined to dynamic code, unsafe to reorder.
|
|
214
|
+
- Interpolated double-quoted strings (`"p-4 {$dynamic} flex"`).
|
|
215
|
+
- Heredoc/nowdoc and backtick (shell-exec) strings, and strings containing escape sequences.
|
|
216
|
+
|
|
217
|
+
**Off by default:** without `tailwindPhpSources` (and `--php-source`), behavior is identical to 0.2.x — no PHP
|
|
218
|
+
declaration is ever touched.
|
|
219
|
+
|
|
220
|
+
## WordPress themes & plugins
|
|
221
|
+
|
|
222
|
+
Most WordPress sorting needs **no opt-in at all**. Template files and partials output markup, and the `class="..."`
|
|
223
|
+
in that markup is sorted by the default HTML pass — even when the value is interrupted by PHP:
|
|
224
|
+
|
|
225
|
+
```php
|
|
226
|
+
<article <?php post_class( 'z-10 flex' ); ?>>
|
|
227
|
+
<h2 class="text-2xl font-bold <?= $featured ? 'text-amber-600' : '' ?> tracking-tight">
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
`tailwindPhpSources` is only for classes you store in **PHP values** — a variant map, a config array, theme defaults.
|
|
231
|
+
For that, **don't opt in a general partial.** Partials are full of non-class strings — `__()` translations,
|
|
232
|
+
`get_template_part()` names, query args, URLs — and an opted-in file sorts _every_ multi-word string value. The Tailwind
|
|
233
|
+
sorter leaves most prose alone (unknown words keep their order), but it **will** reorder any string containing words
|
|
234
|
+
that are also utilities (`grid`, `block`, `flex`, `hidden`, `container`, `table`, …), so `'Switch to grid view'`
|
|
235
|
+
becomes `'Switch to view grid'`.
|
|
236
|
+
|
|
237
|
+
Instead, keep class maps in a **dedicated file** whose every value is a class list, and opt in only that file:
|
|
238
|
+
|
|
239
|
+
```php
|
|
240
|
+
// inc/ui-classes.php → tailwindPhpSources: ['inc/ui-classes.php']
|
|
241
|
+
return array(
|
|
242
|
+
'button' => array(
|
|
243
|
+
'primary' => 'rounded bg-blue-600 px-4 py-2 text-white',
|
|
244
|
+
'secondary' => 'rounded bg-gray-100 px-4 py-2 text-gray-900',
|
|
245
|
+
),
|
|
246
|
+
'card' => 'rounded-lg border bg-white p-6 shadow-sm',
|
|
247
|
+
);
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
`require` that map from your partials. The map file is 100% class strings (safe to sort); the partials stay out of
|
|
251
|
+
`tailwindPhpSources` and get their markup sorted by the HTML pass as usual.
|
|
252
|
+
|
|
149
253
|
## How it handles mixed templates
|
|
150
254
|
|
|
151
255
|
PHP islands inside a class attribute are treated as opaque atoms that never move. Static text between islands is sorted
|
|
152
256
|
independently — the same model the official plugin uses for `${}` interpolations in template literals.
|
|
153
257
|
|
|
258
|
+
<details>
|
|
259
|
+
<summary>Glued-fragment pinning, whitespace handling, and the full edge-case list — click to expand</summary>
|
|
260
|
+
|
|
154
261
|
```php
|
|
155
262
|
<!-- before -->
|
|
156
263
|
<h2 class="text-2xl font-bold <?= $featured ? 'text-amber-600' : '' ?> tracking-tight leading-snug">
|
|
@@ -173,8 +280,12 @@ Also handled correctly:
|
|
|
173
280
|
- `?>` inside `//` and `#` line comments (island ends — genuine PHP behavior)
|
|
174
281
|
- `#[Attributes]`, `<?PHP` case-insensitivity, `<?xml` exclusion, files ending in PHP mode
|
|
175
282
|
- PHP islands as standalone attributes: `<div <?php post_class(); ?> class="...">`
|
|
176
|
-
-
|
|
177
|
-
`class="
|
|
283
|
+
- With `tailwindPhpSources`, string values inside an island in a class attribute
|
|
284
|
+
(`class="p-4 <?= $on ? 'flex z-10' : '' ?>"`) sort in the same single pass
|
|
285
|
+
- `<script>`/`<style>` content, HTML comments, and `echo '<div class="...">'` strings are left alone (to sort class
|
|
286
|
+
strings declared in PHP, see [Sorting classes in PHP declarations](#sorting-classes-in-php-declarations))
|
|
287
|
+
|
|
288
|
+
</details>
|
|
178
289
|
|
|
179
290
|
## Programmatic API
|
|
180
291
|
|
|
@@ -190,6 +301,9 @@ const out = transform(source, sortFn);
|
|
|
190
301
|
|
|
191
302
|
## Known limitations
|
|
192
303
|
|
|
304
|
+
<details>
|
|
305
|
+
<summary>Edge cases and unsupported syntax — click to expand</summary>
|
|
306
|
+
|
|
193
307
|
- Complex string interpolation containing double quotes (`"{$arr["key"]}"`) can desync the PHP string lexer in rare
|
|
194
308
|
cases. Use `{$arr['key']}` style or extract to a variable.
|
|
195
309
|
- Unquoted attribute values (`class=foo`) are skipped.
|
|
@@ -197,6 +311,8 @@ const out = transform(source, sortFn);
|
|
|
197
311
|
classes).
|
|
198
312
|
- Whitespace inside multi-line class attributes is normalized to single spaces (matches Prettier behavior).
|
|
199
313
|
|
|
314
|
+
</details>
|
|
315
|
+
|
|
200
316
|
## Development
|
|
201
317
|
|
|
202
318
|
```sh
|
|
@@ -204,10 +320,11 @@ bun test # or: node --test "test/*.test.ts"
|
|
|
204
320
|
bun run build # compile src → dist (tsc); the published artifact
|
|
205
321
|
```
|
|
206
322
|
|
|
207
|
-
|
|
208
|
-
integration tests that exercise the real `prettier-plugin-tailwindcss` sorter and skip automatically when the
|
|
209
|
-
toolchain isn't installed,
|
|
210
|
-
unavailable
|
|
323
|
+
84 tests: 63 core tests that are dependency-free (the sorter is injected, so they run against a mock `SortFn`),
|
|
324
|
+
7 integration tests that exercise the real `prettier-plugin-tailwindcss` sorter and skip automatically when the
|
|
325
|
+
Tailwind toolchain isn't installed, 8 `init` tests that run against throwaway git repositories and skip when
|
|
326
|
+
`git` is unavailable, and 6 CLI tests covering argument validation, help/version output, and file-scanning rules
|
|
327
|
+
(the scan test also skips without the Tailwind toolchain).
|
|
211
328
|
|
|
212
329
|
## License
|
|
213
330
|
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,18 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* tailwind-sort-php CLI
|
|
3
|
+
* tailwind-sort-php CLI — see `USAGE` for the flag reference.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* Options:
|
|
10
|
-
* --stylesheet <path> Tailwind v4 CSS entry
|
|
11
|
-
* --attr <name> Extra attribute to sort (repeatable)
|
|
12
|
-
* --check Don't write; exit 1 if any file needs sorting
|
|
13
|
-
* --no-short-tags Don't treat bare `<?` as a PHP open tag
|
|
14
|
-
*
|
|
15
|
-
* Defaults to all `.php` files under `cwd` when no globs are given.
|
|
16
|
-
* Skips `node_modules`, `vendor`, `dist` and `.git`. The `init` subcommand installs the pre-commit hook; see `init.ts`.
|
|
5
|
+
* Option values fall back to the resolved Prettier config — the same source of truth `prettier-plugin-tailwindcss`
|
|
6
|
+
* uses; see `fromPrettierConfig()`.
|
|
7
|
+
* The `init` subcommand installs the pre-commit hook; see `init.ts`.
|
|
17
8
|
*/
|
|
18
9
|
export {};
|
package/dist/cli.js
CHANGED
|
@@ -1,24 +1,31 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* tailwind-sort-php CLI
|
|
3
|
+
* tailwind-sort-php CLI — see `USAGE` for the flag reference.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* Options:
|
|
10
|
-
* --stylesheet <path> Tailwind v4 CSS entry
|
|
11
|
-
* --attr <name> Extra attribute to sort (repeatable)
|
|
12
|
-
* --check Don't write; exit 1 if any file needs sorting
|
|
13
|
-
* --no-short-tags Don't treat bare `<?` as a PHP open tag
|
|
14
|
-
*
|
|
15
|
-
* Defaults to all `.php` files under `cwd` when no globs are given.
|
|
16
|
-
* Skips `node_modules`, `vendor`, `dist` and `.git`. The `init` subcommand installs the pre-commit hook; see `init.ts`.
|
|
5
|
+
* Option values fall back to the resolved Prettier config — the same source of truth `prettier-plugin-tailwindcss`
|
|
6
|
+
* uses; see `fromPrettierConfig()`.
|
|
7
|
+
* The `init` subcommand installs the pre-commit hook; see `init.ts`.
|
|
17
8
|
*/
|
|
18
9
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
19
10
|
import { transform } from "./transform.js";
|
|
20
11
|
import { createTailwindSortFn } from "./sorter.js";
|
|
21
12
|
import { runInit } from "./init.js";
|
|
13
|
+
const USAGE = `Usage:
|
|
14
|
+
tailwind-sort-php [options] [glob ...]
|
|
15
|
+
tailwind-sort-php init [--fix] [--force] [--dry-run]
|
|
16
|
+
|
|
17
|
+
Options:
|
|
18
|
+
--stylesheet <path> Tailwind v4 CSS entry (default: tailwindStylesheet from your Prettier config)
|
|
19
|
+
--attr <name> Extra attribute to sort (repeatable; merged with tailwindAttributes)
|
|
20
|
+
--php-source <glob> Also sort class strings in PHP declarations in matching files
|
|
21
|
+
(repeatable; merged with tailwindPhpSources)
|
|
22
|
+
--check Don't write; exit 1 if any file needs sorting
|
|
23
|
+
--no-short-tags Don't treat bare <? as a PHP open tag
|
|
24
|
+
-h, --help Show this help
|
|
25
|
+
--version Print the version
|
|
26
|
+
|
|
27
|
+
Defaults to all .php files under the cwd when no globs are given;
|
|
28
|
+
node_modules, vendor, dist and .git are always skipped.`;
|
|
22
29
|
const IGNORE = ['node_modules', 'vendor', 'dist', '.git'];
|
|
23
30
|
/**
|
|
24
31
|
* Parse command-line arguments.
|
|
@@ -30,21 +37,24 @@ function parseArgs(argv) {
|
|
|
30
37
|
const cli = {
|
|
31
38
|
globs: [],
|
|
32
39
|
attrs: ['class', 'className'],
|
|
40
|
+
phpSources: [],
|
|
33
41
|
check: false,
|
|
34
42
|
shortTags: true,
|
|
35
43
|
};
|
|
36
44
|
for (let i = 0; i < argv.length; i++) {
|
|
37
45
|
const a = argv[i];
|
|
38
|
-
if (a === '--
|
|
46
|
+
if (a === '--stylesheet')
|
|
47
|
+
cli.stylesheet = requireValue(argv, ++i, a);
|
|
48
|
+
else if (a === '--attr')
|
|
49
|
+
cli.attrs.push(requireValue(argv, ++i, a));
|
|
50
|
+
else if (a === '--php-source')
|
|
51
|
+
cli.phpSources.push(requireValue(argv, ++i, a));
|
|
52
|
+
else if (a === '--check')
|
|
39
53
|
cli.check = true;
|
|
40
54
|
else if (a === '--no-short-tags')
|
|
41
55
|
cli.shortTags = false;
|
|
42
|
-
else if (a === '--stylesheet')
|
|
43
|
-
cli.stylesheet = argv[++i];
|
|
44
|
-
else if (a === '--attr')
|
|
45
|
-
cli.attrs.push(argv[++i]);
|
|
46
56
|
else if (a.startsWith('--')) {
|
|
47
|
-
console.error(`Unknown option: ${a}`);
|
|
57
|
+
console.error(`Unknown option: ${a}\n\n${USAGE}`);
|
|
48
58
|
process.exit(2);
|
|
49
59
|
}
|
|
50
60
|
else
|
|
@@ -54,6 +64,17 @@ function parseArgs(argv) {
|
|
|
54
64
|
cli.globs.push('**/*.php');
|
|
55
65
|
return cli;
|
|
56
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Return a flag's value argument or exit with a usage error when it is missing or is itself a flag.
|
|
69
|
+
*/
|
|
70
|
+
function requireValue(argv, i, flag) {
|
|
71
|
+
const v = argv[i];
|
|
72
|
+
if (v === undefined || v.startsWith('--')) {
|
|
73
|
+
console.error(`Missing value for ${flag}`);
|
|
74
|
+
process.exit(2);
|
|
75
|
+
}
|
|
76
|
+
return v;
|
|
77
|
+
}
|
|
57
78
|
/**
|
|
58
79
|
* Yield file paths matching the given globs, using `Bun.Glob` under Bun and `node:fs` glob (Node >= 22) otherwise.
|
|
59
80
|
*
|
|
@@ -69,8 +90,10 @@ async function* scanFiles(globs) {
|
|
|
69
90
|
}
|
|
70
91
|
else {
|
|
71
92
|
const { glob } = await import('node:fs/promises');
|
|
93
|
+
// Prunes ignored directories during traversal; newer Node versions pass a Dirent instead of a path string.
|
|
94
|
+
const exclude = (entry) => ignored(typeof entry === 'string' ? entry : entry.name);
|
|
72
95
|
for (const pattern of globs) {
|
|
73
|
-
for await (const f of glob(pattern))
|
|
96
|
+
for await (const f of glob(pattern, { exclude }))
|
|
74
97
|
yield f;
|
|
75
98
|
}
|
|
76
99
|
}
|
|
@@ -81,13 +104,12 @@ async function* scanFiles(globs) {
|
|
|
81
104
|
* @param file Path to test, relative to `cwd`.
|
|
82
105
|
* @returns True when the path is inside an ignored directory and should be skipped.
|
|
83
106
|
*/
|
|
84
|
-
const ignored = (file) =>
|
|
107
|
+
const ignored = (file) => file.split(/[\\/]/).some((seg) => IGNORE.includes(seg));
|
|
85
108
|
/**
|
|
86
|
-
* Best-effort read of the
|
|
87
|
-
*
|
|
88
|
-
* `tailwindAttributes` (merged into the attribute list).
|
|
109
|
+
* Best-effort read of the resolved Prettier config — the shared source of truth with `prettier-plugin-tailwindcss`.
|
|
110
|
+
* Picks up `tailwindStylesheet`, `tailwindAttributes`, and `tailwindPhpSources`.
|
|
89
111
|
*
|
|
90
|
-
* @returns The
|
|
112
|
+
* @returns The found settings, or an empty object if none are available.
|
|
91
113
|
*/
|
|
92
114
|
async function fromPrettierConfig() {
|
|
93
115
|
try {
|
|
@@ -106,6 +128,9 @@ async function fromPrettierConfig() {
|
|
|
106
128
|
if (Array.isArray(cfg.tailwindAttributes)) {
|
|
107
129
|
out.attributes = cfg.tailwindAttributes.filter((a) => typeof a === 'string' && !a.startsWith('/'));
|
|
108
130
|
}
|
|
131
|
+
if (Array.isArray(cfg.tailwindPhpSources)) {
|
|
132
|
+
out.phpSources = cfg.tailwindPhpSources.filter((p) => typeof p === 'string');
|
|
133
|
+
}
|
|
109
134
|
return out;
|
|
110
135
|
}
|
|
111
136
|
catch {
|
|
@@ -114,6 +139,16 @@ async function fromPrettierConfig() {
|
|
|
114
139
|
}
|
|
115
140
|
async function main() {
|
|
116
141
|
const argv = process.argv.slice(2);
|
|
142
|
+
// Help/version take precedence everywhere, including after `init`.
|
|
143
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
144
|
+
console.log(USAGE);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (argv.includes('--version')) {
|
|
148
|
+
const pkg = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
|
|
149
|
+
console.log(pkg.version);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
117
152
|
if (argv[0] === 'init')
|
|
118
153
|
return runInit(argv.slice(1));
|
|
119
154
|
const cli = parseArgs(argv);
|
|
@@ -135,24 +170,32 @@ async function main() {
|
|
|
135
170
|
attributes: cli.attrs,
|
|
136
171
|
shortOpenTags: cli.shortTags,
|
|
137
172
|
};
|
|
173
|
+
// Pre-scan the opt-in `tailwindPhpSources` globs; a file gets `sortPhpStrings` only if it matches one.
|
|
174
|
+
const phpSources = [...cli.phpSources, ...(pc.phpSources ?? [])];
|
|
175
|
+
const phpSourceFiles = new Set();
|
|
176
|
+
for await (const file of scanFiles(phpSources)) {
|
|
177
|
+
if (!ignored(file))
|
|
178
|
+
phpSourceFiles.add(file);
|
|
179
|
+
}
|
|
138
180
|
let scanned = 0;
|
|
139
181
|
let changed = 0;
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
182
|
+
const seen = new Set();
|
|
183
|
+
// Include php-source files so a designated holder is sorted even outside the main globs; `seen` dedupes.
|
|
184
|
+
for await (const file of scanFiles([...cli.globs, ...phpSources])) {
|
|
185
|
+
if (ignored(file) || seen.has(file))
|
|
186
|
+
continue;
|
|
187
|
+
seen.add(file);
|
|
188
|
+
scanned++;
|
|
189
|
+
const src = await readFile(file, 'utf8');
|
|
190
|
+
const out = transform(src, sortFn, { ...opts, sortPhpStrings: phpSourceFiles.has(file) });
|
|
191
|
+
if (out !== src) {
|
|
192
|
+
changed++;
|
|
193
|
+
if (cli.check) {
|
|
194
|
+
console.log(`needs sorting: ${file}`);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
await writeFile(file, out);
|
|
198
|
+
console.log(`sorted: ${file}`);
|
|
156
199
|
}
|
|
157
200
|
}
|
|
158
201
|
}
|
package/dist/html.js
CHANGED
|
@@ -47,6 +47,7 @@ export function findClassAttributes(masked, opts = {}) {
|
|
|
47
47
|
const wanted = new Set((opts.attributes ?? ['class', 'classname']).map((a) => a.toLowerCase()));
|
|
48
48
|
const out = [];
|
|
49
49
|
const len = masked.length;
|
|
50
|
+
const lower = masked.toLowerCase();
|
|
50
51
|
let i = 0;
|
|
51
52
|
while (i < len) {
|
|
52
53
|
const lt = masked.indexOf('<', i);
|
|
@@ -75,12 +76,12 @@ export function findClassAttributes(masked, opts = {}) {
|
|
|
75
76
|
let j = lt + 1;
|
|
76
77
|
while (j < len && /[A-Za-z0-9:-]/.test(masked[j]))
|
|
77
78
|
j++;
|
|
78
|
-
const tagName =
|
|
79
|
+
const tagName = lower.slice(lt + 1, j);
|
|
79
80
|
j = scanTagAttributes(masked, j, wanted, out);
|
|
80
81
|
// Skip raw-text element content up to its closing tag.
|
|
81
82
|
if (RAW_TEXT_TAGS.has(tagName)) {
|
|
82
83
|
const closer = `</${tagName}`;
|
|
83
|
-
const idx =
|
|
84
|
+
const idx = lower.indexOf(closer, j);
|
|
84
85
|
j = idx === -1 ? len : idx;
|
|
85
86
|
}
|
|
86
87
|
i = j;
|
package/dist/index.d.ts
CHANGED
|
@@ -10,4 +10,5 @@
|
|
|
10
10
|
export { transform, type SortFn, type TransformOptions } from './transform.ts';
|
|
11
11
|
export { findIslands, type Island, type IslandOptions } from './islands.ts';
|
|
12
12
|
export { maskIslands, findClassAttributes, type ClassAttr, type HtmlScanOptions } from './html.ts';
|
|
13
|
+
export { findSortablePhpStrings, type PhpStringRange } from './php-strings.ts';
|
|
13
14
|
export { createTailwindSortFn, type SorterOptions } from './sorter.ts';
|
package/dist/index.js
CHANGED
|
@@ -10,4 +10,5 @@
|
|
|
10
10
|
export { transform } from "./transform.js";
|
|
11
11
|
export { findIslands } from "./islands.js";
|
|
12
12
|
export { maskIslands, findClassAttributes } from "./html.js";
|
|
13
|
+
export { findSortablePhpStrings } from "./php-strings.js";
|
|
13
14
|
export { createTailwindSortFn } from "./sorter.js";
|
package/dist/init.js
CHANGED
|
@@ -132,7 +132,11 @@ export async function runInit(argv) {
|
|
|
132
132
|
let repairMode = false;
|
|
133
133
|
if (current !== null && current !== hookBody) {
|
|
134
134
|
if (!cli.force) {
|
|
135
|
-
const installed = current === HOOK_CHECK
|
|
135
|
+
const installed = current === HOOK_CHECK
|
|
136
|
+
? 'the check variant'
|
|
137
|
+
: current === HOOK_FIX
|
|
138
|
+
? 'the --fix variant'
|
|
139
|
+
: 'a custom hook';
|
|
136
140
|
fail(`${HOOK_PATH} already exists and differs (${installed} is installed). Re-run with --force to overwrite.`);
|
|
137
141
|
}
|
|
138
142
|
writeHook = true;
|
|
@@ -148,7 +152,9 @@ export async function runInit(argv) {
|
|
|
148
152
|
await writeFile(hookAbs, hookBody);
|
|
149
153
|
await chmod(hookAbs, 0o755);
|
|
150
154
|
}
|
|
151
|
-
done.push(cli.dryRun
|
|
155
|
+
done.push(cli.dryRun
|
|
156
|
+
? `would install ${HOOK_PATH} (${variant} variant)`
|
|
157
|
+
: `installed ${HOOK_PATH} (${variant} variant)`);
|
|
152
158
|
}
|
|
153
159
|
if (repairMode) {
|
|
154
160
|
if (!cli.dryRun)
|