@runtimestudio/tailwind-sort-php 0.1.0 → 0.2.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Runtime Studio
3
+ Copyright (c) 2026 The Trustee for 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
@@ -1,7 +1,10 @@
1
- # @runtimestudio/tailwind-sort-php
1
+ # Tailwind CSS Class Sorter for PHP
2
2
 
3
- Tailwind CSS class sorting for **mixed PHP/HTML templates** — the WordPress template-partial case that
4
- `@prettier/plugin-php` + `prettier-plugin-tailwindcss` can't handle.
3
+ [![npm version](https://img.shields.io/npm/v/@runtimestudio/tailwind-sort-php)](https://www.npmjs.com/package/@runtimestudio/tailwind-sort-php)
4
+ [![License: MIT](https://img.shields.io/npm/l/@runtimestudio/tailwind-sort-php)](LICENSE)
5
+
6
+ `@runtimestudio/tailwind-sort-php` sorts Tailwind CSS classes in **plain PHP files, WordPress themes and plugins, and
7
+ mixed PHP/HTML templates** — the case `prettier-plugin-tailwindcss` can't parse and `@prettier/plugin-php` mangles.
5
8
 
6
9
  `prettier-plugin-tailwindcss` sorts classes beautifully, but it can't parse files that interleave PHP with HTML, and
7
10
  `@prettier/plugin-php` reformats the entire PHP file as a side effect. This tool sorts **only** the class attribute
@@ -63,12 +66,15 @@ bunx tailwind-sort-php --check
63
66
 
64
67
  # explicit stylesheet (overrides the Prettier config)
65
68
  bunx tailwind-sort-php --stylesheet ./resources/css/main.css
69
+
70
+ # one-time: install the pre-commit hook (see "Pre-commit gate" below)
71
+ bunx tailwind-sort-php init
66
72
  ```
67
73
 
68
74
  ### Options
69
75
 
70
76
  | Flag | Description |
71
- | --------------------- | ------------------------------------------------------------------------------------------------- |
77
+ |-----------------------|---------------------------------------------------------------------------------------------------|
72
78
  | `--stylesheet <path>` | Tailwind v4 CSS entry. Defaults to `tailwindStylesheet` from your Prettier config. |
73
79
  | `--attr <name>` | Extra attribute to sort (repeatable). Merged with `tailwindAttributes` from your Prettier config. |
74
80
  | `--check` | Don't write; exit 1 if any file needs sorting. |
@@ -76,6 +82,68 @@ bunx tailwind-sort-php --stylesheet ./resources/css/main.css
76
82
 
77
83
  Default globs are all `.php` files under the cwd; `node_modules`, `vendor`, `dist`, and `.git` are always skipped.
78
84
 
85
+ ## Editor integration
86
+
87
+ No IDE plugin is needed — two small setups cover the common workflows.
88
+
89
+ ### Sort on save (PhpStorm / IntelliJ)
90
+
91
+ Add a File Watcher (Settings → Tools → File Watchers → `+` → Custom):
92
+
93
+ - **File type:** PHP
94
+ - **Program:** `$ProjectFileDir$/node_modules/.bin/tailwind-sort-php`
95
+ - **Arguments:** `$FilePathRelativeToProjectRoot$`
96
+ - **Working directory:** `$ProjectFileDir$`
97
+
98
+ Untick "Auto-save edited files to trigger the watcher" so it runs on explicit save (~130 ms per file). The definition
99
+ lives in `.idea/watcherTasks.xml`, which you can commit to share it with your team.
100
+
101
+ ### Sort on save (VS Code)
102
+
103
+ Install the [Run on Save](https://marketplace.visualstudio.com/items?itemName=emeraldwalk.RunOnSave) extension, then add
104
+ to `.vscode/settings.json`:
105
+
106
+ ```json
107
+ {
108
+ "emeraldwalk.runonsave": {
109
+ "commands": [
110
+ {
111
+ "match": "\\.php$",
112
+ "cmd": "${workspaceFolder}/node_modules/.bin/tailwind-sort-php ${relativeFile}"
113
+ }
114
+ ]
115
+ }
116
+ }
117
+ ```
118
+
119
+ ### Pre-commit gate
120
+
121
+ Keep unsorted classes from landing regardless of the editor. One command installs a dependency-free Git hook at
122
+ `.githooks/pre-commit` and points `core.hooksPath` at it:
123
+
124
+ ```sh
125
+ # check-and-fail (default): names the unsorted files and blocks the commit
126
+ npx tailwind-sort-php init
127
+
128
+ # auto-fix: sorts the staged files in place, then blocks the commit for review and re-staging
129
+ npx tailwind-sort-php init --fix
130
+ ```
131
+
132
+ `init` is no-clobber by default: it refuses to overwrite a differing hook, repoint a `core.hooksPath` that's set
133
+ elsewhere (husky etc.), or disable hooks already living in `.git/hooks` — pass `--force` to override, `--dry-run` to
134
+ preview. Run it once per clone; commit the `.githooks/` directory to share the hook with your team.
135
+
136
+ Both variants check working-tree file contents, so with partial staging (`git add -p`) the hook can mis-report — and
137
+ under `--fix`, re-staging a fixed file can pull in unrelated unstaged hunks.
138
+
139
+ Wiring the gate into your own hook manager (husky, lefthook) instead? The staged-PHP check is this one-liner:
140
+
141
+ ```sh
142
+ git diff --cached --name-only -z --diff-filter=ACMR -- '*.php' | xargs -0 ./node_modules/.bin/tailwind-sort-php --check
143
+ ```
144
+
145
+ In CI there's no staged diff — just sweep the whole project with `npx tailwind-sort-php --check`.
146
+
79
147
  ## How it handles mixed templates
80
148
 
81
149
  PHP islands inside a class attribute are treated as opaque atoms that never move. Static text between islands is sorted
@@ -131,11 +199,14 @@ const out = transform(source, sortFn);
131
199
 
132
200
  ```sh
133
201
  bun test # or: node --test "test/*.test.ts"
202
+ bun run build # compile src → dist (tsc); the published artifact
134
203
  ```
135
204
 
136
- 41 tests, zero dependencies the sorter is injected (`SortFn`), so tests run with a mock alphabetical sorter and the
137
- official sorter is only loaded by the CLI.
205
+ 54 tests: 41 core tests that are dependency-free (the sorter is injected, so they run against a mock `SortFn`), 5
206
+ integration tests that exercise the real `prettier-plugin-tailwindcss` sorter and skip automatically when the Tailwind
207
+ toolchain isn't installed, and 8 `init` tests that run against throwaway git repositories and skip when `git` is
208
+ unavailable.
138
209
 
139
210
  ## License
140
211
 
141
- MIT © Runtime Studio
212
+ [MIT](LICENSE) © Runtime Studio
package/dist/cli.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * tailwind-sort-php CLI
4
+ *
5
+ * Usage:
6
+ * tailwind-sort-php [options] [glob ...]
7
+ * tailwind-sort-php init [--fix] [--force] [--dry-run]
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` (`"**" + "/*.php"`) when no globs are given.
16
+ * Skips `node_modules`, `vendor`, `dist` and `.git`. The `init` subcommand installs the pre-commit hook; see `init.ts`.
17
+ */
18
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * tailwind-sort-php CLI
4
+ *
5
+ * Usage:
6
+ * tailwind-sort-php [options] [glob ...]
7
+ * tailwind-sort-php init [--fix] [--force] [--dry-run]
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` (`"**" + "/*.php"`) when no globs are given.
16
+ * Skips `node_modules`, `vendor`, `dist` and `.git`. The `init` subcommand installs the pre-commit hook; see `init.ts`.
17
+ */
18
+ import { readFile, writeFile } from 'node:fs/promises';
19
+ import { transform } from "./transform.js";
20
+ import { createTailwindSortFn } from "./sorter.js";
21
+ import { runInit } from "./init.js";
22
+ const IGNORE = ['node_modules', 'vendor', 'dist', '.git'];
23
+ /**
24
+ * Parse command-line arguments.
25
+ *
26
+ * @param argv Arguments after the runtime and script path.
27
+ * @returns Parsed CLI options with defaults applied.
28
+ */
29
+ function parseArgs(argv) {
30
+ const cli = {
31
+ globs: [],
32
+ attrs: ['class', 'className'],
33
+ check: false,
34
+ shortTags: true,
35
+ };
36
+ for (let i = 0; i < argv.length; i++) {
37
+ const a = argv[i];
38
+ if (a === '--check')
39
+ cli.check = true;
40
+ else if (a === '--no-short-tags')
41
+ 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
+ else if (a.startsWith('--')) {
47
+ console.error(`Unknown option: ${a}`);
48
+ process.exit(2);
49
+ }
50
+ else
51
+ cli.globs.push(a);
52
+ }
53
+ if (cli.globs.length === 0)
54
+ cli.globs.push('**/*.php');
55
+ return cli;
56
+ }
57
+ /**
58
+ * Yield file paths matching the given globs, using `Bun.Glob` under Bun and `node:fs` glob (Node >= 22) otherwise.
59
+ *
60
+ * @param globs Glob patterns relative to `cwd`.
61
+ */
62
+ async function* scanFiles(globs) {
63
+ // Use `Bun.Glob` when available, fall back to `node:fs` glob (Node 22+).
64
+ if (typeof globalThis.Bun !== 'undefined') {
65
+ const { Glob } = await import('bun');
66
+ for (const pattern of globs) {
67
+ for await (const f of new Glob(pattern).scan('.'))
68
+ yield f;
69
+ }
70
+ }
71
+ else {
72
+ const { glob } = await import('node:fs/promises');
73
+ for (const pattern of globs) {
74
+ for await (const f of glob(pattern))
75
+ yield f;
76
+ }
77
+ }
78
+ }
79
+ /**
80
+ * Whether a path falls inside an always-ignored directory.
81
+ *
82
+ * @param file Path to test, relative to `cwd`.
83
+ * @returns True when the path is inside an ignored directory and should be skipped.
84
+ */
85
+ const ignored = (file) => IGNORE.some((d) => file.includes(`${d}/`) || file.startsWith(d));
86
+ /**
87
+ * Best-effort read of the project's resolved Prettier config, so this tool shares one source of truth with
88
+ * `prettier-plugin-tailwindcss`. Picks up `tailwindStylesheet` (resolved relative to the config file) and
89
+ * `tailwindAttributes` (merged into the attribute list).
90
+ *
91
+ * @returns The resolved stylesheet path and attributes, or an empty object if none are available.
92
+ */
93
+ async function fromPrettierConfig() {
94
+ try {
95
+ const prettier = await import('prettier');
96
+ const { dirname, resolve } = await import('node:path');
97
+ const configFile = await prettier.resolveConfigFile();
98
+ if (!configFile)
99
+ return {};
100
+ const cfg = (await prettier.resolveConfig(configFile));
101
+ if (!cfg)
102
+ return {};
103
+ const out = {};
104
+ if (typeof cfg.tailwindStylesheet === 'string') {
105
+ out.stylesheet = resolve(dirname(configFile), cfg.tailwindStylesheet);
106
+ }
107
+ if (Array.isArray(cfg.tailwindAttributes)) {
108
+ out.attributes = cfg.tailwindAttributes.filter((a) => typeof a === 'string' && !a.startsWith('/'));
109
+ }
110
+ return out;
111
+ }
112
+ catch {
113
+ return {}; // prettier not installed or config unreadable — flags only
114
+ }
115
+ }
116
+ async function main() {
117
+ const argv = process.argv.slice(2);
118
+ if (argv[0] === 'init')
119
+ return runInit(argv.slice(1));
120
+ const cli = parseArgs(argv);
121
+ const pc = await fromPrettierConfig();
122
+ const stylesheet = cli.stylesheet ?? pc.stylesheet;
123
+ if (!stylesheet) {
124
+ console.error('No Tailwind stylesheet found. Pass --stylesheet <path> or set ' +
125
+ '`tailwindStylesheet` in your Prettier config.');
126
+ process.exit(2);
127
+ }
128
+ if (pc.attributes) {
129
+ for (const a of pc.attributes) {
130
+ if (!cli.attrs.includes(a))
131
+ cli.attrs.push(a);
132
+ }
133
+ }
134
+ const sortFn = await createTailwindSortFn({ stylesheet });
135
+ const opts = {
136
+ attributes: cli.attrs,
137
+ shortOpenTags: cli.shortTags,
138
+ };
139
+ let scanned = 0;
140
+ let changed = 0;
141
+ for (const pattern of cli.globs) {
142
+ for await (const file of scanFiles([pattern])) {
143
+ if (ignored(file))
144
+ continue;
145
+ scanned++;
146
+ const src = await readFile(file, 'utf8');
147
+ const out = transform(src, sortFn, opts);
148
+ if (out !== src) {
149
+ changed++;
150
+ if (cli.check) {
151
+ console.log(`needs sorting: ${file}`);
152
+ }
153
+ else {
154
+ await writeFile(file, out);
155
+ console.log(`sorted: ${file}`);
156
+ }
157
+ }
158
+ }
159
+ }
160
+ console.log(`${scanned} file(s) scanned, ${changed} ${cli.check ? 'need(s) sorting' : 'updated'}`);
161
+ if (cli.check && changed > 0)
162
+ process.exit(1);
163
+ }
164
+ main().catch((err) => {
165
+ console.error(err);
166
+ process.exit(1);
167
+ });
package/dist/html.d.ts ADDED
@@ -0,0 +1,59 @@
1
+ /**
2
+ * HTML attribute scanner — the second pass of the two-pass lexer.
3
+ *
4
+ * Operates on a "masked" copy of the source in which every PHP island byte has been replaced with `\x00`
5
+ * (offsets preserved). This means:
6
+ * - quotes/angle brackets inside PHP can never confuse the HTML scan
7
+ * - islands inside a tag read as attribute separators
8
+ * - islands inside a quoted attribute value read as opaque atoms
9
+ *
10
+ * Skips HTML comments, doctype/CDATA, and the raw-text content of `script`/`style`/`textarea`/`title` elements —
11
+ * their `script`/`style` content may contain strings like `class="..."` that must not be touched.
12
+ *
13
+ * @see islands.ts - first pass; produces the islands consumed here.
14
+ */
15
+ import type { Island } from './islands.ts';
16
+ /**
17
+ * Location of a sortable class attribute value within the source.
18
+ */
19
+ export interface ClassAttr {
20
+ /**
21
+ * Attribute name as written (e.g. `class`, `className`).
22
+ */
23
+ name: string;
24
+ /**
25
+ * Offset of the first character inside the quotes.
26
+ */
27
+ valueStart: number;
28
+ /**
29
+ * Offset just past the last character inside the quotes (exclusive).
30
+ */
31
+ valueEnd: number;
32
+ }
33
+ /**
34
+ * Options controlling which attributes are collected.
35
+ */
36
+ export interface HtmlScanOptions {
37
+ /**
38
+ * Lowercase attribute names to collect; default `['class', 'classname']`.
39
+ */
40
+ attributes?: string[];
41
+ }
42
+ /**
43
+ * Produce a copy of the source with every island byte replaced by `\x00`.
44
+ *
45
+ * Length and offsets are preserved, so positions found in the masked string map 1:1 back to the original source.
46
+ *
47
+ * @param src Original template source.
48
+ * @param islands Island ranges from `findIslands()`.
49
+ * @returns Masked source of identical length.
50
+ */
51
+ export declare function maskIslands(src: string, islands: Island[]): string;
52
+ /**
53
+ * Locate every sortable class attribute in an island-masked source.
54
+ *
55
+ * @param masked Source pre-processed by `maskIslands()`.
56
+ * @param opts Attribute collection options.
57
+ * @returns Attribute value locations in document order. Offsets index into the original source.
58
+ */
59
+ export declare function findClassAttributes(masked: string, opts?: HtmlScanOptions): ClassAttr[];
package/dist/html.js ADDED
@@ -0,0 +1,145 @@
1
+ /**
2
+ * HTML attribute scanner — the second pass of the two-pass lexer.
3
+ *
4
+ * Operates on a "masked" copy of the source in which every PHP island byte has been replaced with `\x00`
5
+ * (offsets preserved). This means:
6
+ * - quotes/angle brackets inside PHP can never confuse the HTML scan
7
+ * - islands inside a tag read as attribute separators
8
+ * - islands inside a quoted attribute value read as opaque atoms
9
+ *
10
+ * Skips HTML comments, doctype/CDATA, and the raw-text content of `script`/`style`/`textarea`/`title` elements —
11
+ * their `script`/`style` content may contain strings like `class="..."` that must not be touched.
12
+ *
13
+ * @see islands.ts - first pass; produces the islands consumed here.
14
+ */
15
+ const RAW_TEXT_TAGS = new Set(['script', 'style', 'textarea', 'title']);
16
+ const NUL = '\x00';
17
+ /**
18
+ * Produce a copy of the source with every island byte replaced by `\x00`.
19
+ *
20
+ * Length and offsets are preserved, so positions found in the masked string map 1:1 back to the original source.
21
+ *
22
+ * @param src Original template source.
23
+ * @param islands Island ranges from `findIslands()`.
24
+ * @returns Masked source of identical length.
25
+ */
26
+ export function maskIslands(src, islands) {
27
+ if (islands.length === 0)
28
+ return src;
29
+ let out = '';
30
+ let pos = 0;
31
+ for (const isl of islands) {
32
+ out += src.slice(pos, isl.start);
33
+ out += NUL.repeat(isl.end - isl.start);
34
+ pos = isl.end;
35
+ }
36
+ out += src.slice(pos);
37
+ return out;
38
+ }
39
+ /**
40
+ * Locate every sortable class attribute in an island-masked source.
41
+ *
42
+ * @param masked Source pre-processed by `maskIslands()`.
43
+ * @param opts Attribute collection options.
44
+ * @returns Attribute value locations in document order. Offsets index into the original source.
45
+ */
46
+ export function findClassAttributes(masked, opts = {}) {
47
+ const wanted = new Set((opts.attributes ?? ['class', 'classname']).map((a) => a.toLowerCase()));
48
+ const out = [];
49
+ const len = masked.length;
50
+ let i = 0;
51
+ while (i < len) {
52
+ const lt = masked.indexOf('<', i);
53
+ if (lt === -1)
54
+ break;
55
+ // HTML comment.
56
+ if (masked.startsWith('<!--', lt)) {
57
+ const close = masked.indexOf('-->', lt + 4);
58
+ i = close === -1 ? len : close + 3;
59
+ continue;
60
+ }
61
+ // Doctype / CDATA / other declarations.
62
+ if (masked[lt + 1] === '!') {
63
+ const close = masked.indexOf('>', lt + 2);
64
+ i = close === -1 ? len : close + 1;
65
+ continue;
66
+ }
67
+ // Closing tag.
68
+ if (masked[lt + 1] === '/') {
69
+ const close = masked.indexOf('>', lt + 2);
70
+ i = close === -1 ? len : close + 1;
71
+ continue;
72
+ }
73
+ // Opening tag?
74
+ if (lt + 1 < len && /[A-Za-z]/.test(masked[lt + 1])) {
75
+ let j = lt + 1;
76
+ while (j < len && /[A-Za-z0-9:-]/.test(masked[j]))
77
+ j++;
78
+ const tagName = masked.slice(lt + 1, j).toLowerCase();
79
+ j = scanTagAttributes(masked, j, wanted, out);
80
+ // Skip raw-text element content up to its closing tag.
81
+ if (RAW_TEXT_TAGS.has(tagName)) {
82
+ const closer = `</${tagName}`;
83
+ const idx = masked.toLowerCase().indexOf(closer, j);
84
+ j = idx === -1 ? len : idx;
85
+ }
86
+ i = j;
87
+ continue;
88
+ }
89
+ i = lt + 1;
90
+ }
91
+ return out;
92
+ }
93
+ const isTagWs = (c) => c === ' ' || c === '\t' || c === '\n' || c === '\r' || c === '\f' || c === NUL;
94
+ /**
95
+ * Parse attributes from just past the tag name to just past `>`.
96
+ * Returns the offset after `>` (or EOF). Pushes matches into `out`.
97
+ */
98
+ function scanTagAttributes(masked, i, wanted, out) {
99
+ const len = masked.length;
100
+ while (i < len) {
101
+ while (i < len && isTagWs(masked[i]))
102
+ i++;
103
+ if (i >= len)
104
+ return len;
105
+ const c = masked[i];
106
+ if (c === '>')
107
+ return i + 1;
108
+ if (c === '/') {
109
+ i++;
110
+ continue;
111
+ }
112
+ // Attribute name.
113
+ const nameStart = i;
114
+ while (i < len && !isTagWs(masked[i]) && masked[i] !== '=' && masked[i] !== '>' && masked[i] !== '/')
115
+ i++;
116
+ const name = masked.slice(nameStart, i);
117
+ if (name.length === 0) {
118
+ i++;
119
+ continue;
120
+ }
121
+ while (i < len && isTagWs(masked[i]))
122
+ i++;
123
+ if (masked[i] !== '=')
124
+ continue; // boolean attribute
125
+ i++;
126
+ while (i < len && isTagWs(masked[i]))
127
+ i++;
128
+ const q = masked[i];
129
+ if (q === '"' || q === "'") {
130
+ const valueStart = i + 1;
131
+ const close = masked.indexOf(q, valueStart);
132
+ const valueEnd = close === -1 ? len : close;
133
+ if (wanted.has(name.toLowerCase())) {
134
+ out.push({ name, valueStart, valueEnd });
135
+ }
136
+ i = valueEnd + 1;
137
+ }
138
+ else {
139
+ // Unquoted value — read it but never rewrite (too risky to widen).
140
+ while (i < len && !isTagWs(masked[i]) && masked[i] !== '>')
141
+ i++;
142
+ }
143
+ }
144
+ return len;
145
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Public API for `@runtimestudio/tailwind-sort-php`.
3
+ *
4
+ * Tailwind CSS class sorting for mixed PHP/HTML templates. The core is dependency-free;
5
+ * the official sorting engine is wired in via `createTailwindSortFn()` or any custom `SortFn`.
6
+ *
7
+ * @packageDocumentation
8
+ * @see cli.ts - command-line interface built on this API.
9
+ */
10
+ export { transform, type SortFn, type TransformOptions } from './transform.ts';
11
+ export { findIslands, type Island, type IslandOptions } from './islands.ts';
12
+ export { maskIslands, findClassAttributes, type ClassAttr, type HtmlScanOptions } from './html.ts';
13
+ export { createTailwindSortFn, type SorterOptions } from './sorter.ts';
package/dist/index.js ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Public API for `@runtimestudio/tailwind-sort-php`.
3
+ *
4
+ * Tailwind CSS class sorting for mixed PHP/HTML templates. The core is dependency-free;
5
+ * the official sorting engine is wired in via `createTailwindSortFn()` or any custom `SortFn`.
6
+ *
7
+ * @packageDocumentation
8
+ * @see cli.ts - command-line interface built on this API.
9
+ */
10
+ export { transform } from "./transform.js";
11
+ export { findIslands } from "./islands.js";
12
+ export { maskIslands, findClassAttributes } from "./html.js";
13
+ export { createTailwindSortFn } from "./sorter.js";
package/dist/init.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * tailwind-sort-php init
3
+ *
4
+ * Installs the pre-commit hook: writes `.githooks/pre-commit` and points `core.hooksPath` at it.
5
+ * No-clobber unless `--force`; `--fix` installs the auto-fixing variant; `--dry-run` previews.
6
+ */
7
+ /**
8
+ * Run the `init` subcommand: install the pre-commit hook and point `core.hooksPath` at it.
9
+ *
10
+ * @param argv Arguments after the `init` subcommand name.
11
+ */
12
+ export declare function runInit(argv: string[]): Promise<void>;