@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/package.json CHANGED
@@ -1,57 +1,57 @@
1
1
  {
2
- "name": "@runtimestudio/tailwind-sort-php",
3
- "version": "0.2.1",
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
- }
2
+ "name": "@runtimestudio/tailwind-sort-php",
3
+ "version": "0.4.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.9",
43
+ "prettier-plugin-tailwindcss": "^0.8",
44
+ "tailwindcss": "^4.3",
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
@@ -1,19 +1,10 @@
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
- * 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` 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
 
19
10
  import { readFile, writeFile } from 'node:fs/promises';
@@ -21,14 +12,32 @@ import { transform, type TransformOptions } from './transform.ts';
21
12
  import { createTailwindSortFn } from './sorter.ts';
22
13
  import { runInit } from './init.ts';
23
14
 
15
+ const USAGE = `Usage:
16
+ tailwind-sort-php [options] [glob ...]
17
+ tailwind-sort-php init [--fix] [--force] [--dry-run]
18
+
19
+ Options:
20
+ --stylesheet <path> Tailwind v4 CSS entry (default: tailwindStylesheet from your Prettier config)
21
+ --attr <name> Extra attribute to sort (repeatable; merged with tailwindAttributes)
22
+ --php-source <glob> Also sort class strings in PHP declarations in matching files
23
+ (repeatable; merged with tailwindPhpSources)
24
+ --check Don't write; exit 1 if any file needs sorting
25
+ --no-short-tags Don't treat bare <? as a PHP open tag
26
+ -h, --help Show this help
27
+ --version Print the version
28
+
29
+ Defaults to all .php files under the cwd when no globs are given;
30
+ node_modules, vendor, dist and .git are always skipped.`;
31
+
24
32
  const IGNORE = ['node_modules', 'vendor', 'dist', '.git'];
25
33
 
26
34
  interface Cli {
27
- globs: string[];
28
- stylesheet?: string;
29
- attrs: string[];
30
- check: boolean;
31
- shortTags: boolean;
35
+ globs: string[];
36
+ stylesheet?: string;
37
+ attrs: string[];
38
+ phpSources: string[];
39
+ check: boolean;
40
+ shortTags: boolean;
32
41
  }
33
42
 
34
43
  /**
@@ -38,25 +47,39 @@ interface Cli {
38
47
  * @returns Parsed CLI options with defaults applied.
39
48
  */
40
49
  function parseArgs(argv: string[]): Cli {
41
- const cli: Cli = {
42
- globs: [],
43
- attrs: ['class', 'className'],
44
- check: false,
45
- shortTags: true,
46
- };
47
- for (let i = 0; i < argv.length; i++) {
48
- const a = argv[i];
49
- if (a === '--check') cli.check = true;
50
- else if (a === '--no-short-tags') cli.shortTags = false;
51
- else if (a === '--stylesheet') cli.stylesheet = argv[++i];
52
- else if (a === '--attr') cli.attrs.push(argv[++i]);
53
- else if (a.startsWith('--')) {
54
- console.error(`Unknown option: ${a}`);
55
- process.exit(2);
56
- } else cli.globs.push(a);
57
- }
58
- if (cli.globs.length === 0) cli.globs.push('**/*.php');
59
- return cli;
50
+ const cli: Cli = {
51
+ globs: [],
52
+ attrs: ['class', 'className'],
53
+ phpSources: [],
54
+ check: false,
55
+ shortTags: true,
56
+ };
57
+ for (let i = 0; i < argv.length; i++) {
58
+ const a = argv[i];
59
+ if (a === '--stylesheet') cli.stylesheet = requireValue(argv, ++i, a);
60
+ else if (a === '--attr') cli.attrs.push(requireValue(argv, ++i, a));
61
+ else if (a === '--php-source') cli.phpSources.push(requireValue(argv, ++i, a));
62
+ else if (a === '--check') cli.check = true;
63
+ else if (a === '--no-short-tags') cli.shortTags = false;
64
+ else if (a.startsWith('--')) {
65
+ console.error(`Unknown option: ${a}\n\n${USAGE}`);
66
+ process.exit(2);
67
+ } else cli.globs.push(a);
68
+ }
69
+ if (cli.globs.length === 0) cli.globs.push('**/*.php');
70
+ return cli;
71
+ }
72
+
73
+ /**
74
+ * Return a flag's value argument or exit with a usage error when it is missing or is itself a flag.
75
+ */
76
+ function requireValue(argv: string[], i: number, flag: string): string {
77
+ const v = argv[i];
78
+ if (v === undefined || v.startsWith('--')) {
79
+ console.error(`Missing value for ${flag}`);
80
+ process.exit(2);
81
+ }
82
+ return v;
60
83
  }
61
84
 
62
85
  /**
@@ -65,17 +88,19 @@ function parseArgs(argv: string[]): Cli {
65
88
  * @param globs Glob patterns relative to `cwd`.
66
89
  */
67
90
  async function* scanFiles(globs: string[]): AsyncGenerator<string> {
68
- if (typeof (globalThis as any).Bun !== 'undefined') {
69
- const { Glob } = await import('bun');
70
- for (const pattern of globs) {
71
- for await (const f of new Glob(pattern).scan('.')) yield f;
72
- }
73
- } else {
74
- const { glob } = await import('node:fs/promises');
75
- for (const pattern of globs) {
76
- for await (const f of glob(pattern)) yield f as string;
91
+ if (typeof (globalThis as any).Bun !== 'undefined') {
92
+ const { Glob } = await import('bun');
93
+ for (const pattern of globs) {
94
+ for await (const f of new Glob(pattern).scan('.')) yield f;
95
+ }
96
+ } else {
97
+ const { glob } = await import('node:fs/promises');
98
+ // Prunes ignored directories during traversal; newer Node versions pass a Dirent instead of a path string.
99
+ const exclude = (entry: string | { name: string }) => ignored(typeof entry === 'string' ? entry : entry.name);
100
+ for (const pattern of globs) {
101
+ for await (const f of glob(pattern, { exclude })) yield f as string;
102
+ }
77
103
  }
78
- }
79
104
  }
80
105
 
81
106
  /**
@@ -84,92 +109,117 @@ async function* scanFiles(globs: string[]): AsyncGenerator<string> {
84
109
  * @param file Path to test, relative to `cwd`.
85
110
  * @returns True when the path is inside an ignored directory and should be skipped.
86
111
  */
87
- const ignored = (file: string) => IGNORE.some((d) => file.includes(`${d}/`) || file.startsWith(d));
112
+ const ignored = (file: string) => file.split(/[\\/]/).some((seg) => IGNORE.includes(seg));
88
113
 
89
114
  /**
90
- * Best-effort read of the project's resolved Prettier config, so this tool shares one source of truth with
91
- * `prettier-plugin-tailwindcss`. Picks up `tailwindStylesheet` (resolved relative to the config file) and
92
- * `tailwindAttributes` (merged into the attribute list).
115
+ * Best-effort read of the resolved Prettier config the shared source of truth with `prettier-plugin-tailwindcss`.
116
+ * Picks up `tailwindStylesheet`, `tailwindAttributes`, and `tailwindPhpSources`.
93
117
  *
94
- * @returns The resolved stylesheet path and attributes, or an empty object if none are available.
118
+ * @returns The found settings, or an empty object if none are available.
95
119
  */
96
120
  async function fromPrettierConfig(): Promise<{
97
- stylesheet?: string;
98
- attributes?: string[];
121
+ stylesheet?: string;
122
+ attributes?: string[];
123
+ phpSources?: string[];
99
124
  }> {
100
- try {
101
- const prettier = await import('prettier');
102
- const { dirname, resolve } = await import('node:path');
103
- const configFile = await prettier.resolveConfigFile();
104
- if (!configFile) return {};
105
- const cfg = (await prettier.resolveConfig(configFile)) as Record<string, unknown> | null;
106
- if (!cfg) return {};
107
- const out: { stylesheet?: string; attributes?: string[] } = {};
108
- if (typeof cfg.tailwindStylesheet === 'string') {
109
- out.stylesheet = resolve(dirname(configFile), cfg.tailwindStylesheet);
110
- }
111
- if (Array.isArray(cfg.tailwindAttributes)) {
112
- out.attributes = cfg.tailwindAttributes.filter((a): a is string => typeof a === 'string' && !a.startsWith('/'));
125
+ try {
126
+ const prettier = await import('prettier');
127
+ const { dirname, resolve } = await import('node:path');
128
+ const configFile = await prettier.resolveConfigFile();
129
+ if (!configFile) return {};
130
+ const cfg = (await prettier.resolveConfig(configFile)) as Record<string, unknown> | null;
131
+ if (!cfg) return {};
132
+ const out: { stylesheet?: string; attributes?: string[]; phpSources?: string[] } = {};
133
+ if (typeof cfg.tailwindStylesheet === 'string') {
134
+ out.stylesheet = resolve(dirname(configFile), cfg.tailwindStylesheet);
135
+ }
136
+ if (Array.isArray(cfg.tailwindAttributes)) {
137
+ out.attributes = cfg.tailwindAttributes.filter(
138
+ (a): a is string => typeof a === 'string' && !a.startsWith('/'),
139
+ );
140
+ }
141
+ if (Array.isArray(cfg.tailwindPhpSources)) {
142
+ out.phpSources = cfg.tailwindPhpSources.filter((p): p is string => typeof p === 'string');
143
+ }
144
+ return out;
145
+ } catch {
146
+ return {}; // prettier not installed or config unreadable — flags only
113
147
  }
114
- return out;
115
- } catch {
116
- return {}; // prettier not installed or config unreadable — flags only
117
- }
118
148
  }
119
149
 
120
150
  async function main() {
121
- const argv = process.argv.slice(2);
122
- if (argv[0] === 'init') return runInit(argv.slice(1));
123
-
124
- const cli = parseArgs(argv);
125
-
126
- const pc = await fromPrettierConfig();
127
- const stylesheet = cli.stylesheet ?? pc.stylesheet;
128
- if (!stylesheet) {
129
- console.error(
130
- 'No Tailwind stylesheet found. Pass --stylesheet <path> or set ' +
131
- '`tailwindStylesheet` in your Prettier config.',
132
- );
133
- process.exit(2);
134
- }
135
- if (pc.attributes) {
136
- for (const a of pc.attributes) {
137
- if (!cli.attrs.includes(a)) cli.attrs.push(a);
151
+ const argv = process.argv.slice(2);
152
+
153
+ // Help/version take precedence everywhere, including after `init`.
154
+ if (argv.includes('--help') || argv.includes('-h')) {
155
+ console.log(USAGE);
156
+ return;
157
+ }
158
+ if (argv.includes('--version')) {
159
+ const pkg = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
160
+ console.log(pkg.version);
161
+ return;
162
+ }
163
+
164
+ if (argv[0] === 'init') return runInit(argv.slice(1));
165
+
166
+ const cli = parseArgs(argv);
167
+
168
+ const pc = await fromPrettierConfig();
169
+ const stylesheet = cli.stylesheet ?? pc.stylesheet;
170
+ if (!stylesheet) {
171
+ console.error(
172
+ 'No Tailwind stylesheet found. Pass --stylesheet <path> or set ' +
173
+ '`tailwindStylesheet` in your Prettier config.',
174
+ );
175
+ process.exit(2);
138
176
  }
139
- }
140
-
141
- const sortFn = await createTailwindSortFn({ stylesheet });
142
- const opts: TransformOptions = {
143
- attributes: cli.attrs,
144
- shortOpenTags: cli.shortTags,
145
- };
146
-
147
- let scanned = 0;
148
- let changed = 0;
149
-
150
- for (const pattern of cli.globs) {
151
- for await (const file of scanFiles([pattern])) {
152
- if (ignored(file)) continue;
153
- scanned++;
154
- const src = await readFile(file, 'utf8');
155
- const out = transform(src, sortFn, opts);
156
- if (out !== src) {
157
- changed++;
158
- if (cli.check) {
159
- console.log(`needs sorting: ${file}`);
160
- } else {
161
- await writeFile(file, out);
162
- console.log(`sorted: ${file}`);
177
+ if (pc.attributes) {
178
+ for (const a of pc.attributes) {
179
+ if (!cli.attrs.includes(a)) cli.attrs.push(a);
180
+ }
181
+ }
182
+
183
+ const sortFn = await createTailwindSortFn({ stylesheet });
184
+ const opts: TransformOptions = {
185
+ attributes: cli.attrs,
186
+ shortOpenTags: cli.shortTags,
187
+ };
188
+
189
+ // Pre-scan the opt-in `tailwindPhpSources` globs; a file gets `sortPhpStrings` only if it matches one.
190
+ const phpSources = [...cli.phpSources, ...(pc.phpSources ?? [])];
191
+ const phpSourceFiles = new Set<string>();
192
+ for await (const file of scanFiles(phpSources)) {
193
+ if (!ignored(file)) phpSourceFiles.add(file);
194
+ }
195
+
196
+ let scanned = 0;
197
+ let changed = 0;
198
+ const seen = new Set<string>();
199
+
200
+ // Include php-source files so a designated holder is sorted even outside the main globs; `seen` dedupes.
201
+ for await (const file of scanFiles([...cli.globs, ...phpSources])) {
202
+ if (ignored(file) || seen.has(file)) continue;
203
+ seen.add(file);
204
+ scanned++;
205
+ const src = await readFile(file, 'utf8');
206
+ const out = transform(src, sortFn, { ...opts, sortPhpStrings: phpSourceFiles.has(file) });
207
+ if (out !== src) {
208
+ changed++;
209
+ if (cli.check) {
210
+ console.log(`needs sorting: ${file}`);
211
+ } else {
212
+ await writeFile(file, out);
213
+ console.log(`sorted: ${file}`);
214
+ }
163
215
  }
164
- }
165
216
  }
166
- }
167
217
 
168
- console.log(`${scanned} file(s) scanned, ${changed} ${cli.check ? 'need(s) sorting' : 'updated'}`);
169
- if (cli.check && changed > 0) process.exit(1);
218
+ console.log(`${scanned} file(s) scanned, ${changed} ${cli.check ? 'need(s) sorting' : 'updated'}`);
219
+ if (cli.check && changed > 0) process.exit(1);
170
220
  }
171
221
 
172
222
  main().catch((err) => {
173
- console.error(err);
174
- process.exit(1);
223
+ console.error(err);
224
+ process.exit(1);
175
225
  });