@runtimestudio/tailwind-sort-php 0.2.0 → 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 +150 -36
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +35 -22
- package/dist/html.d.ts +1 -1
- package/dist/html.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/init.js +13 -9
- package/dist/islands.js +1 -1
- 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 +122 -106
- package/src/html.ts +119 -119
- package/src/index.ts +1 -0
- package/src/init.ts +102 -96
- 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/README.md
CHANGED
|
@@ -18,19 +18,28 @@ 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.
|
|
24
33
|
- `prettier` ≥ 3 and `prettier-plugin-tailwindcss` ≥ 0.8 (peer dependencies).
|
|
25
34
|
|
|
26
35
|
## Install
|
|
27
36
|
|
|
28
37
|
```sh
|
|
29
|
-
# Bun
|
|
30
|
-
bun add -D @runtimestudio/tailwind-sort-php prettier prettier-plugin-tailwindcss
|
|
31
|
-
|
|
32
38
|
# npm
|
|
33
39
|
npm install -D @runtimestudio/tailwind-sort-php prettier prettier-plugin-tailwindcss
|
|
40
|
+
|
|
41
|
+
# Bun
|
|
42
|
+
bun add -D @runtimestudio/tailwind-sort-php prettier prettier-plugin-tailwindcss
|
|
34
43
|
```
|
|
35
44
|
|
|
36
45
|
pnpm and yarn work the same (`pnpm add -D …` / `yarn add -D …`).
|
|
@@ -38,12 +47,14 @@ pnpm and yarn work the same (`pnpm add -D …` / `yarn add -D …`).
|
|
|
38
47
|
## Setup
|
|
39
48
|
|
|
40
49
|
Point Prettier at your Tailwind v4 entry stylesheet so both `prettier-plugin-tailwindcss` and this tool share one
|
|
41
|
-
vocabulary.
|
|
50
|
+
vocabulary. Any config format Prettier supports works (`.prettierrc`, `prettier.config.js`, a `package.json`
|
|
51
|
+
`"prettier"` key, …) — the tool reads the resolved config, exactly like the plugin does. For example, in
|
|
52
|
+
`prettier.config.mjs`:
|
|
42
53
|
|
|
43
54
|
```js
|
|
44
55
|
export default {
|
|
45
|
-
|
|
46
|
-
|
|
56
|
+
plugins: ['prettier-plugin-tailwindcss'],
|
|
57
|
+
tailwindStylesheet: './resources/css/main.css',
|
|
47
58
|
};
|
|
48
59
|
```
|
|
49
60
|
|
|
@@ -52,39 +63,43 @@ this in place, the CLI needs no flags.
|
|
|
52
63
|
|
|
53
64
|
## Usage
|
|
54
65
|
|
|
55
|
-
Run with `
|
|
66
|
+
Run with `npx` (Node ≥ 22.18) or `bunx` (Bun):
|
|
56
67
|
|
|
57
68
|
```sh
|
|
58
69
|
# sort every .php file under the cwd (stylesheet read from your Prettier config)
|
|
59
|
-
|
|
70
|
+
npx tailwind-sort-php
|
|
60
71
|
|
|
61
72
|
# specific globs
|
|
62
|
-
|
|
73
|
+
npx tailwind-sort-php "template-parts/**/*.php" "*.php"
|
|
63
74
|
|
|
64
75
|
# CI / pre-commit — write nothing, exit 1 if anything is unsorted
|
|
65
|
-
|
|
76
|
+
npx tailwind-sort-php --check
|
|
66
77
|
|
|
67
78
|
# explicit stylesheet (overrides the Prettier config)
|
|
68
|
-
|
|
79
|
+
npx tailwind-sort-php --stylesheet ./resources/css/main.css
|
|
69
80
|
|
|
70
81
|
# one-time: install the pre-commit hook (see "Pre-commit gate" below)
|
|
71
|
-
|
|
82
|
+
npx tailwind-sort-php init
|
|
72
83
|
```
|
|
73
84
|
|
|
74
85
|
### Options
|
|
75
86
|
|
|
76
|
-
| Flag | Description
|
|
77
|
-
|
|
78
|
-
| `--stylesheet <path>` | Tailwind v4 CSS entry. Defaults to `tailwindStylesheet` from your Prettier config.
|
|
79
|
-
| `--attr <name>` | Extra attribute to sort (repeatable). Merged with `tailwindAttributes` from your Prettier config.
|
|
80
|
-
| `--
|
|
81
|
-
| `--
|
|
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. |
|
|
82
94
|
|
|
83
95
|
Default globs are all `.php` files under the cwd; `node_modules`, `vendor`, `dist`, and `.git` are always skipped.
|
|
84
96
|
|
|
85
97
|
## Editor integration
|
|
86
98
|
|
|
87
|
-
No IDE plugin is needed — two small setups cover the common workflows.
|
|
99
|
+
No IDE plugin is needed — two small setups cover the common workflows: sort-on-save and a pre-commit gate.
|
|
100
|
+
|
|
101
|
+
<details>
|
|
102
|
+
<summary><b>Sort on save</b> (PhpStorm / IntelliJ, VS Code) and a <b>pre-commit gate</b> — click to expand</summary>
|
|
88
103
|
|
|
89
104
|
### Sort on save (PhpStorm / IntelliJ)
|
|
90
105
|
|
|
@@ -105,14 +120,14 @@ to `.vscode/settings.json`:
|
|
|
105
120
|
|
|
106
121
|
```json
|
|
107
122
|
{
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
123
|
+
"emeraldwalk.runonsave": {
|
|
124
|
+
"commands": [
|
|
125
|
+
{
|
|
126
|
+
"match": "\\.php$",
|
|
127
|
+
"cmd": "${workspaceFolder}/node_modules/.bin/tailwind-sort-php ${relativeFile}"
|
|
128
|
+
}
|
|
129
|
+
]
|
|
130
|
+
}
|
|
116
131
|
}
|
|
117
132
|
```
|
|
118
133
|
|
|
@@ -144,11 +159,103 @@ git diff --cached --name-only -z --diff-filter=ACMR -- '*.php' | xargs -0 ./node
|
|
|
144
159
|
|
|
145
160
|
In CI there's no staged diff — just sweep the whole project with `npx tailwind-sort-php --check`.
|
|
146
161
|
|
|
162
|
+
</details>
|
|
163
|
+
|
|
164
|
+
## Sorting classes in PHP declarations
|
|
165
|
+
|
|
166
|
+
By default, the tool only sorts classes inside HTML `class="..."` attributes. Class strings declared in **PHP itself** —
|
|
167
|
+
constants, static properties, config arrays — sit inside PHP code the tool treats as opaque, so they're left alone.
|
|
168
|
+
|
|
169
|
+
Opt in **per file** by listing the files whose PHP string values are Tailwind class lists, via `tailwindPhpSources` in
|
|
170
|
+
your Prettier config (or the repeatable `--php-source <glob>` flag):
|
|
171
|
+
|
|
172
|
+
```js
|
|
173
|
+
export default {
|
|
174
|
+
plugins: ['prettier-plugin-tailwindcss'],
|
|
175
|
+
tailwindStylesheet: './resources/css/main.css',
|
|
176
|
+
tailwindPhpSources: ['src/classes/*.php'],
|
|
177
|
+
};
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
With this set, every **string value** in a matched file is sorted with the exact same engine and order as the HTML
|
|
181
|
+
side. In `key => value` arrays only the **value** is sorted — keys are never touched:
|
|
182
|
+
|
|
183
|
+
```php
|
|
184
|
+
// before
|
|
185
|
+
public const array VARIANTS = array(
|
|
186
|
+
'primary' => 'text-white px-4 bg-blue-600 rounded py-2',
|
|
187
|
+
'secondary' => 'text-gray-900 px-4 bg-gray-100 rounded py-2',
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
// after
|
|
191
|
+
public const array VARIANTS = array(
|
|
192
|
+
'primary' => 'rounded bg-blue-600 px-4 py-2 text-white',
|
|
193
|
+
'secondary' => 'rounded bg-gray-100 px-4 py-2 text-gray-900',
|
|
194
|
+
);
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Scalar declarations work the same way (`const string CARD = '...'`, `static $x = '...'`, `$x = '...'`), as do nested
|
|
198
|
+
and list-style arrays.
|
|
199
|
+
|
|
200
|
+
> **⚠️ Point `tailwindPhpSources` only at files whose string values are all Tailwind class lists.** The tool does
|
|
201
|
+
> **not** guess whether a string "looks like" classes — within a matched file it sorts **every** eligible string value.
|
|
202
|
+
> Aimed at a general file, it **will** reorder the words inside non-class strings (labels, URLs, SQL). This is a
|
|
203
|
+
> deliberate design contract, not a bug: safety comes from your file-level opt-in, which is why a dedicated
|
|
204
|
+
> directory of class-holder files (e.g. `src/classes/`) is the intended target.
|
|
205
|
+
|
|
206
|
+
The opt-in is **inert at runtime** — it lives in formatter config only, never in your source. Your PHP stays vanilla,
|
|
207
|
+
with zero coupling to this tool (no marker comments, no helper functions, no attributes).
|
|
208
|
+
|
|
209
|
+
**Skipped automatically** (left byte-identical, even in a matched file):
|
|
210
|
+
|
|
211
|
+
- Concatenated literals (`'btn-' . $variant`) — a fragment joined to dynamic code, unsafe to reorder.
|
|
212
|
+
- Interpolated double-quoted strings (`"p-4 {$dynamic} flex"`).
|
|
213
|
+
- Heredoc/nowdoc and backtick (shell-exec) strings, and strings containing escape sequences.
|
|
214
|
+
|
|
215
|
+
**Off by default:** without `tailwindPhpSources` (and `--php-source`), behavior is identical to 0.2.x — no PHP
|
|
216
|
+
declaration is ever touched.
|
|
217
|
+
|
|
218
|
+
## WordPress themes & plugins
|
|
219
|
+
|
|
220
|
+
Most WordPress sorting needs **no opt-in at all**. Template files and partials output markup, and the `class="..."`
|
|
221
|
+
in that markup is sorted by the default HTML pass — even when the value is interrupted by PHP:
|
|
222
|
+
|
|
223
|
+
```php
|
|
224
|
+
<article <?php post_class( 'z-10 flex' ); ?>>
|
|
225
|
+
<h2 class="text-2xl font-bold <?= $featured ? 'text-amber-600' : '' ?> tracking-tight">
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
`tailwindPhpSources` is only for classes you store in **PHP values** — a variant map, a config array, theme defaults.
|
|
229
|
+
For that, **don't opt in a general partial.** Partials are full of non-class strings — `__()` translations,
|
|
230
|
+
`get_template_part()` names, query args, URLs — and an opted-in file sorts _every_ multi-word string value. The Tailwind
|
|
231
|
+
sorter leaves most prose alone (unknown words keep their order), but it **will** reorder any string containing words
|
|
232
|
+
that are also utilities (`grid`, `block`, `flex`, `hidden`, `container`, `table`, …), so `'Switch to grid view'`
|
|
233
|
+
becomes `'Switch to view grid'`.
|
|
234
|
+
|
|
235
|
+
Instead, keep class maps in a **dedicated file** whose every value is a class list, and opt in only that file:
|
|
236
|
+
|
|
237
|
+
```php
|
|
238
|
+
// inc/ui-classes.php → tailwindPhpSources: ['inc/ui-classes.php']
|
|
239
|
+
return array(
|
|
240
|
+
'button' => array(
|
|
241
|
+
'primary' => 'rounded bg-blue-600 px-4 py-2 text-white',
|
|
242
|
+
'secondary' => 'rounded bg-gray-100 px-4 py-2 text-gray-900',
|
|
243
|
+
),
|
|
244
|
+
'card' => 'rounded-lg border bg-white p-6 shadow-sm',
|
|
245
|
+
);
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
`require` that map from your partials. The map file is 100% class strings (safe to sort); the partials stay out of
|
|
249
|
+
`tailwindPhpSources` and get their markup sorted by the HTML pass as usual.
|
|
250
|
+
|
|
147
251
|
## How it handles mixed templates
|
|
148
252
|
|
|
149
253
|
PHP islands inside a class attribute are treated as opaque atoms that never move. Static text between islands is sorted
|
|
150
254
|
independently — the same model the official plugin uses for `${}` interpolations in template literals.
|
|
151
255
|
|
|
256
|
+
<details>
|
|
257
|
+
<summary>Glued-fragment pinning, whitespace handling, and the full edge-case list — click to expand</summary>
|
|
258
|
+
|
|
152
259
|
```php
|
|
153
260
|
<!-- before -->
|
|
154
261
|
<h2 class="text-2xl font-bold <?= $featured ? 'text-amber-600' : '' ?> tracking-tight leading-snug">
|
|
@@ -171,8 +278,10 @@ Also handled correctly:
|
|
|
171
278
|
- `?>` inside `//` and `#` line comments (island ends — genuine PHP behavior)
|
|
172
279
|
- `#[Attributes]`, `<?PHP` case-insensitivity, `<?xml` exclusion, files ending in PHP mode
|
|
173
280
|
- PHP islands as standalone attributes: `<div <?php post_class(); ?> class="...">`
|
|
174
|
-
- `<script>`/`<style>` content, HTML comments, and `echo '<div class="...">'` strings are left alone (
|
|
175
|
-
|
|
281
|
+
- `<script>`/`<style>` content, HTML comments, and `echo '<div class="...">'` strings are left alone (to sort class
|
|
282
|
+
strings declared in PHP, see [Sorting classes in PHP declarations](#sorting-classes-in-php-declarations))
|
|
283
|
+
|
|
284
|
+
</details>
|
|
176
285
|
|
|
177
286
|
## Programmatic API
|
|
178
287
|
|
|
@@ -188,6 +297,9 @@ const out = transform(source, sortFn);
|
|
|
188
297
|
|
|
189
298
|
## Known limitations
|
|
190
299
|
|
|
300
|
+
<details>
|
|
301
|
+
<summary>Edge cases and unsupported syntax — click to expand</summary>
|
|
302
|
+
|
|
191
303
|
- Complex string interpolation containing double quotes (`"{$arr["key"]}"`) can desync the PHP string lexer in rare
|
|
192
304
|
cases. Use `{$arr['key']}` style or extract to a variable.
|
|
193
305
|
- Unquoted attribute values (`class=foo`) are skipped.
|
|
@@ -195,17 +307,19 @@ const out = transform(source, sortFn);
|
|
|
195
307
|
classes).
|
|
196
308
|
- Whitespace inside multi-line class attributes is normalized to single spaces (matches Prettier behavior).
|
|
197
309
|
|
|
310
|
+
</details>
|
|
311
|
+
|
|
198
312
|
## Development
|
|
199
313
|
|
|
200
314
|
```sh
|
|
201
|
-
bun test
|
|
202
|
-
bun run build
|
|
315
|
+
bun test # or: node --test "test/*.test.ts"
|
|
316
|
+
bun run build # compile src → dist (tsc); the published artifact
|
|
203
317
|
```
|
|
204
318
|
|
|
205
|
-
|
|
206
|
-
integration tests that exercise the real `prettier-plugin-tailwindcss` sorter and skip automatically when the
|
|
207
|
-
toolchain isn't installed, and 8 `init` tests that run against throwaway git repositories and skip when
|
|
208
|
-
unavailable.
|
|
319
|
+
74 tests: 59 core tests that are dependency-free (the sorter is injected, so they run against a mock `SortFn`),
|
|
320
|
+
7 integration tests that exercise the real `prettier-plugin-tailwindcss` sorter and skip automatically when the
|
|
321
|
+
Tailwind toolchain isn't installed, and 8 `init` tests that run against throwaway git repositories and skip when
|
|
322
|
+
`git` is unavailable.
|
|
209
323
|
|
|
210
324
|
## License
|
|
211
325
|
|
package/dist/cli.d.ts
CHANGED
|
@@ -9,10 +9,11 @@
|
|
|
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
|
*
|
|
15
|
-
* Defaults to all `.php` files under `cwd`
|
|
16
|
+
* Defaults to all `.php` files under `cwd` when no globs are given.
|
|
16
17
|
* Skips `node_modules`, `vendor`, `dist` and `.git`. The `init` subcommand installs the pre-commit hook; see `init.ts`.
|
|
17
18
|
*/
|
|
18
19
|
export {};
|
package/dist/cli.js
CHANGED
|
@@ -9,10 +9,11 @@
|
|
|
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
|
*
|
|
15
|
-
* Defaults to all `.php` files under `cwd`
|
|
16
|
+
* Defaults to all `.php` files under `cwd` when no globs are given.
|
|
16
17
|
* Skips `node_modules`, `vendor`, `dist` and `.git`. The `init` subcommand installs the pre-commit hook; see `init.ts`.
|
|
17
18
|
*/
|
|
18
19
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
@@ -30,6 +31,7 @@ function parseArgs(argv) {
|
|
|
30
31
|
const cli = {
|
|
31
32
|
globs: [],
|
|
32
33
|
attrs: ['class', 'className'],
|
|
34
|
+
phpSources: [],
|
|
33
35
|
check: false,
|
|
34
36
|
shortTags: true,
|
|
35
37
|
};
|
|
@@ -43,6 +45,8 @@ function parseArgs(argv) {
|
|
|
43
45
|
cli.stylesheet = argv[++i];
|
|
44
46
|
else if (a === '--attr')
|
|
45
47
|
cli.attrs.push(argv[++i]);
|
|
48
|
+
else if (a === '--php-source')
|
|
49
|
+
cli.phpSources.push(argv[++i]);
|
|
46
50
|
else if (a.startsWith('--')) {
|
|
47
51
|
console.error(`Unknown option: ${a}`);
|
|
48
52
|
process.exit(2);
|
|
@@ -60,7 +64,6 @@ function parseArgs(argv) {
|
|
|
60
64
|
* @param globs Glob patterns relative to `cwd`.
|
|
61
65
|
*/
|
|
62
66
|
async function* scanFiles(globs) {
|
|
63
|
-
// Use `Bun.Glob` when available, fall back to `node:fs` glob (Node 22+).
|
|
64
67
|
if (typeof globalThis.Bun !== 'undefined') {
|
|
65
68
|
const { Glob } = await import('bun');
|
|
66
69
|
for (const pattern of globs) {
|
|
@@ -84,11 +87,10 @@ async function* scanFiles(globs) {
|
|
|
84
87
|
*/
|
|
85
88
|
const ignored = (file) => IGNORE.some((d) => file.includes(`${d}/`) || file.startsWith(d));
|
|
86
89
|
/**
|
|
87
|
-
* Best-effort read of the
|
|
88
|
-
*
|
|
89
|
-
* `tailwindAttributes` (merged into the attribute list).
|
|
90
|
+
* Best-effort read of the resolved Prettier config — the shared source of truth with `prettier-plugin-tailwindcss`.
|
|
91
|
+
* Picks up `tailwindStylesheet`, `tailwindAttributes`, and `tailwindPhpSources`.
|
|
90
92
|
*
|
|
91
|
-
* @returns The
|
|
93
|
+
* @returns The found settings, or an empty object if none are available.
|
|
92
94
|
*/
|
|
93
95
|
async function fromPrettierConfig() {
|
|
94
96
|
try {
|
|
@@ -107,6 +109,9 @@ async function fromPrettierConfig() {
|
|
|
107
109
|
if (Array.isArray(cfg.tailwindAttributes)) {
|
|
108
110
|
out.attributes = cfg.tailwindAttributes.filter((a) => typeof a === 'string' && !a.startsWith('/'));
|
|
109
111
|
}
|
|
112
|
+
if (Array.isArray(cfg.tailwindPhpSources)) {
|
|
113
|
+
out.phpSources = cfg.tailwindPhpSources.filter((p) => typeof p === 'string');
|
|
114
|
+
}
|
|
110
115
|
return out;
|
|
111
116
|
}
|
|
112
117
|
catch {
|
|
@@ -136,24 +141,32 @@ async function main() {
|
|
|
136
141
|
attributes: cli.attrs,
|
|
137
142
|
shortOpenTags: cli.shortTags,
|
|
138
143
|
};
|
|
144
|
+
// Pre-scan the opt-in `tailwindPhpSources` globs; a file gets `sortPhpStrings` only if it matches one.
|
|
145
|
+
const phpSources = [...cli.phpSources, ...(pc.phpSources ?? [])];
|
|
146
|
+
const phpSourceFiles = new Set();
|
|
147
|
+
for await (const file of scanFiles(phpSources)) {
|
|
148
|
+
if (!ignored(file))
|
|
149
|
+
phpSourceFiles.add(file);
|
|
150
|
+
}
|
|
139
151
|
let scanned = 0;
|
|
140
152
|
let changed = 0;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
153
|
+
const seen = new Set();
|
|
154
|
+
// Include php-source files so a designated holder is sorted even outside the main globs; `seen` dedupes.
|
|
155
|
+
for await (const file of scanFiles([...cli.globs, ...phpSources])) {
|
|
156
|
+
if (ignored(file) || seen.has(file))
|
|
157
|
+
continue;
|
|
158
|
+
seen.add(file);
|
|
159
|
+
scanned++;
|
|
160
|
+
const src = await readFile(file, 'utf8');
|
|
161
|
+
const out = transform(src, sortFn, { ...opts, sortPhpStrings: phpSourceFiles.has(file) });
|
|
162
|
+
if (out !== src) {
|
|
163
|
+
changed++;
|
|
164
|
+
if (cli.check) {
|
|
165
|
+
console.log(`needs sorting: ${file}`);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
await writeFile(file, out);
|
|
169
|
+
console.log(`sorted: ${file}`);
|
|
157
170
|
}
|
|
158
171
|
}
|
|
159
172
|
}
|
package/dist/html.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - islands inside a quoted attribute value read as opaque atoms
|
|
9
9
|
*
|
|
10
10
|
* Skips HTML comments, doctype/CDATA, and the raw-text content of `script`/`style`/`textarea`/`title` elements —
|
|
11
|
-
* their
|
|
11
|
+
* their content may contain strings like `class="..."` that must not be touched.
|
|
12
12
|
*
|
|
13
13
|
* @see islands.ts - first pass; produces the islands consumed here.
|
|
14
14
|
*/
|
package/dist/html.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - islands inside a quoted attribute value read as opaque atoms
|
|
9
9
|
*
|
|
10
10
|
* Skips HTML comments, doctype/CDATA, and the raw-text content of `script`/`style`/`textarea`/`title` elements —
|
|
11
|
-
* their
|
|
11
|
+
* their content may contain strings like `class="..."` that must not be touched.
|
|
12
12
|
*
|
|
13
13
|
* @see islands.ts - first pass; produces the islands consumed here.
|
|
14
14
|
*/
|
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
|
@@ -13,9 +13,8 @@ const HOOK_PATH = `${HOOKS_DIR}/pre-commit`;
|
|
|
13
13
|
* Check-and-fail hook (default): names the unsorted files and blocks the commit; never writes.
|
|
14
14
|
*/
|
|
15
15
|
const HOOK_CHECK = `#!/bin/sh
|
|
16
|
-
# Block commits with unsorted Tailwind classes in staged PHP files.
|
|
17
|
-
#
|
|
18
|
-
# contents, so partial staging (git add -p) can mis-report; see README.
|
|
16
|
+
# Block commits with unsorted Tailwind classes in staged PHP files. Installed by \`tailwind-sort-php init\`.
|
|
17
|
+
# Note: checks working-tree file contents, so partial staging (\`git add -p\`) can mis-report; see README.
|
|
19
18
|
sorter=./node_modules/.bin/tailwind-sort-php
|
|
20
19
|
[ -x "$sorter" ] || exit 0
|
|
21
20
|
git diff --cached --name-only --diff-filter=ACMR -- '*.php' | grep -q . || exit 0
|
|
@@ -31,10 +30,9 @@ exit 1
|
|
|
31
30
|
* Auto-fix hook (--fix): sorts the staged files in place, then blocks the commit for review.
|
|
32
31
|
*/
|
|
33
32
|
const HOOK_FIX = `#!/bin/sh
|
|
34
|
-
# Sort Tailwind classes in staged PHP files, then abort the commit so the
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
# (git add -p), re-staging can pull in unrelated unstaged hunks; see README.
|
|
33
|
+
# Sort Tailwind classes in staged PHP files, then abort the commit so the changes can be reviewed and re-staged.
|
|
34
|
+
# Rewrites working-tree files. Installed by \`tailwind-sort-php init --fix\`.
|
|
35
|
+
# Note: with partial staging (\`git add -p\`), re-staging can pull in unrelated unstaged hunks; see README.
|
|
38
36
|
sorter=./node_modules/.bin/tailwind-sort-php
|
|
39
37
|
[ -x "$sorter" ] || exit 0
|
|
40
38
|
git diff --cached --name-only --diff-filter=ACMR -- '*.php' | grep -q . || exit 0
|
|
@@ -134,7 +132,11 @@ export async function runInit(argv) {
|
|
|
134
132
|
let repairMode = false;
|
|
135
133
|
if (current !== null && current !== hookBody) {
|
|
136
134
|
if (!cli.force) {
|
|
137
|
-
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';
|
|
138
140
|
fail(`${HOOK_PATH} already exists and differs (${installed} is installed). Re-run with --force to overwrite.`);
|
|
139
141
|
}
|
|
140
142
|
writeHook = true;
|
|
@@ -150,7 +152,9 @@ export async function runInit(argv) {
|
|
|
150
152
|
await writeFile(hookAbs, hookBody);
|
|
151
153
|
await chmod(hookAbs, 0o755);
|
|
152
154
|
}
|
|
153
|
-
done.push(cli.dryRun
|
|
155
|
+
done.push(cli.dryRun
|
|
156
|
+
? `would install ${HOOK_PATH} (${variant} variant)`
|
|
157
|
+
: `installed ${HOOK_PATH} (${variant} variant)`);
|
|
154
158
|
}
|
|
155
159
|
if (repairMode) {
|
|
156
160
|
if (!cli.dryRun)
|
package/dist/islands.js
CHANGED
|
@@ -73,7 +73,7 @@ function scanPhpBody(src, i) {
|
|
|
73
73
|
continue;
|
|
74
74
|
}
|
|
75
75
|
// Double-quoted string.
|
|
76
|
-
// Limitation: nested double quotes in complex `{$a["k"]}` interpolation can desync the lexer
|
|
76
|
+
// Limitation: nested double quotes in complex `{$a["k"]}` interpolation can desync the lexer; see README.
|
|
77
77
|
if (c === '"') {
|
|
78
78
|
i = scanQuoted(src, i + 1, '"');
|
|
79
79
|
continue;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PHP string-literal harvester — the optional third pass.
|
|
3
|
+
*
|
|
4
|
+
* Given the source and the PHP islands already found by `findIslands()`, this locates the byte ranges of every
|
|
5
|
+
* string literal *value* inside PHP code that is eligible for class sorting, applying these structural rules:
|
|
6
|
+
*
|
|
7
|
+
* - Only the *value* of a `key => value` pair is eligible; array keys are never sorted.
|
|
8
|
+
* - Bare list-style elements (`['a', 'b']`) and scalar assignments (`const X = '...'`) are values.
|
|
9
|
+
* - Strings that are part of a concatenation expression (`'btn-' . $v`) are skipped — a literal joined to dynamic
|
|
10
|
+
* code may be a partial class fragment, and reordering it would corrupt the rendered string.
|
|
11
|
+
* - Double-quoted strings containing interpolation (`"p-4 {$x}"`) are skipped, mirroring the HTML side's
|
|
12
|
+
* conservatism around dynamic content.
|
|
13
|
+
* - Strings whose body contains a backslash escape are skipped, so escape sequences can never be mangled.
|
|
14
|
+
* - Heredoc/nowdoc and backtick (shell-exec) strings are never harvested.
|
|
15
|
+
*
|
|
16
|
+
* This pass is opt-in per file (the caller decides which files are class-string holders); it does NOT judge whether
|
|
17
|
+
* a given string "looks like" Tailwind classes — within a matched file, every eligible value is sorted.
|
|
18
|
+
*
|
|
19
|
+
* @see islands.ts - first pass; produces the islands consumed here.
|
|
20
|
+
* @see transform.ts - splices the sorted strings back via the shared byte-replacement path.
|
|
21
|
+
*/
|
|
22
|
+
import type { Island } from './islands.ts';
|
|
23
|
+
/**
|
|
24
|
+
* Inner byte range of a sortable string literal — the span *between* the quotes, in original-source offsets.
|
|
25
|
+
*/
|
|
26
|
+
export interface PhpStringRange {
|
|
27
|
+
/**
|
|
28
|
+
* Offset of the first character inside the quotes.
|
|
29
|
+
*/
|
|
30
|
+
start: number;
|
|
31
|
+
/**
|
|
32
|
+
* Offset just past the last character inside the quotes (exclusive).
|
|
33
|
+
*/
|
|
34
|
+
end: number;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Find every sortable string-literal value within the given PHP islands.
|
|
38
|
+
*
|
|
39
|
+
* @param src Original template source.
|
|
40
|
+
* @param islands Island ranges from `findIslands()`.
|
|
41
|
+
* @returns Inner ranges of eligible string values, in document order.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* const islands = findIslands(`<?php $x = 'z a'; ?>`);
|
|
45
|
+
* findSortablePhpStrings(`<?php $x = 'z a'; ?>`, islands); // [{ start: 11, end: 14 }]
|
|
46
|
+
*/
|
|
47
|
+
export declare function findSortablePhpStrings(src: string, islands: Island[]): PhpStringRange[];
|