@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 +1 -1
- package/README.md +78 -7
- package/dist/cli.d.ts +18 -0
- package/dist/cli.js +167 -0
- package/dist/html.d.ts +59 -0
- package/dist/html.js +145 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +13 -0
- package/dist/init.d.ts +12 -0
- package/dist/init.js +171 -0
- package/dist/islands.d.ts +49 -0
- package/dist/islands.js +206 -0
- package/dist/sorter.d.ts +31 -0
- package/dist/sorter.js +26 -0
- package/dist/transform.d.ts +44 -0
- package/dist/transform.js +108 -0
- package/package.json +12 -5
- package/src/cli.ts +7 -2
- package/src/init.ts +188 -0
package/src/cli.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Usage:
|
|
6
6
|
* tailwind-sort-php [options] [glob ...]
|
|
7
|
+
* tailwind-sort-php init [--fix] [--force] [--dry-run]
|
|
7
8
|
*
|
|
8
9
|
* Options:
|
|
9
10
|
* --stylesheet <path> Tailwind v4 CSS entry
|
|
@@ -12,12 +13,13 @@
|
|
|
12
13
|
* --no-short-tags Don't treat bare `<?` as a PHP open tag
|
|
13
14
|
*
|
|
14
15
|
* Defaults to all `.php` files under `cwd` (`"**" + "/*.php"`) when no globs are given.
|
|
15
|
-
* Skips `node_modules`, `vendor`, `dist` and `.git`.
|
|
16
|
+
* Skips `node_modules`, `vendor`, `dist` and `.git`. The `init` subcommand installs the pre-commit hook; see `init.ts`.
|
|
16
17
|
*/
|
|
17
18
|
|
|
18
19
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
19
20
|
import { transform, type TransformOptions } from './transform.ts';
|
|
20
21
|
import { createTailwindSortFn } from './sorter.ts';
|
|
22
|
+
import { runInit } from './init.ts';
|
|
21
23
|
|
|
22
24
|
const IGNORE = ['node_modules', 'vendor', 'dist', '.git'];
|
|
23
25
|
|
|
@@ -117,7 +119,10 @@ async function fromPrettierConfig(): Promise<{
|
|
|
117
119
|
}
|
|
118
120
|
|
|
119
121
|
async function main() {
|
|
120
|
-
const
|
|
122
|
+
const argv = process.argv.slice(2);
|
|
123
|
+
if (argv[0] === 'init') return runInit(argv.slice(1));
|
|
124
|
+
|
|
125
|
+
const cli = parseArgs(argv);
|
|
121
126
|
|
|
122
127
|
const pc = await fromPrettierConfig();
|
|
123
128
|
const stylesheet = cli.stylesheet ?? pc.stylesheet;
|
package/src/init.ts
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
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
|
+
import { execFileSync } from 'node:child_process';
|
|
9
|
+
import { chmod, mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
|
|
12
|
+
const HOOKS_DIR = '.githooks';
|
|
13
|
+
const HOOK_PATH = `${HOOKS_DIR}/pre-commit`;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check-and-fail hook (default): names the unsorted files and blocks the commit; never writes.
|
|
17
|
+
*/
|
|
18
|
+
const HOOK_CHECK = `#!/bin/sh
|
|
19
|
+
# Block commits with unsorted Tailwind classes in staged PHP files.
|
|
20
|
+
# Installed by \`tailwind-sort-php init\`. Note: checks working-tree file
|
|
21
|
+
# contents, so partial staging (git add -p) can mis-report; see README.
|
|
22
|
+
sorter=./node_modules/.bin/tailwind-sort-php
|
|
23
|
+
[ -x "$sorter" ] || exit 0
|
|
24
|
+
git diff --cached --name-only --diff-filter=ACMR -- '*.php' | grep -q . || exit 0
|
|
25
|
+
if git diff --cached --name-only -z --diff-filter=ACMR -- '*.php' | xargs -0 "$sorter" --check; then
|
|
26
|
+
exit 0
|
|
27
|
+
fi
|
|
28
|
+
echo >&2
|
|
29
|
+
echo "Unsorted Tailwind classes in staged PHP (see above)." >&2
|
|
30
|
+
echo "Fix with: npx tailwind-sort-php (or: bunx tailwind-sort-php), then re-stage." >&2
|
|
31
|
+
exit 1
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Auto-fix hook (--fix): sorts the staged files in place, then blocks the commit for review.
|
|
36
|
+
*/
|
|
37
|
+
const HOOK_FIX = `#!/bin/sh
|
|
38
|
+
# Sort Tailwind classes in staged PHP files, then abort the commit so the
|
|
39
|
+
# changes can be reviewed and re-staged. Rewrites working-tree files.
|
|
40
|
+
# Installed by \`tailwind-sort-php init --fix\`. Note: with partial staging
|
|
41
|
+
# (git add -p), re-staging can pull in unrelated unstaged hunks; see README.
|
|
42
|
+
sorter=./node_modules/.bin/tailwind-sort-php
|
|
43
|
+
[ -x "$sorter" ] || exit 0
|
|
44
|
+
git diff --cached --name-only --diff-filter=ACMR -- '*.php' | grep -q . || exit 0
|
|
45
|
+
if git diff --cached --name-only -z --diff-filter=ACMR -- '*.php' | xargs -0 "$sorter" --check >/dev/null 2>&1; then
|
|
46
|
+
exit 0
|
|
47
|
+
fi
|
|
48
|
+
git diff --cached --name-only -z --diff-filter=ACMR -- '*.php' | xargs -0 "$sorter"
|
|
49
|
+
echo >&2
|
|
50
|
+
echo "Sorted Tailwind classes in staged PHP (see above)." >&2
|
|
51
|
+
echo "Review the changes, re-stage, and commit again." >&2
|
|
52
|
+
exit 1
|
|
53
|
+
`;
|
|
54
|
+
|
|
55
|
+
interface InitCli {
|
|
56
|
+
fix: boolean;
|
|
57
|
+
force: boolean;
|
|
58
|
+
dryRun: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Parse `init` subcommand arguments.
|
|
63
|
+
*
|
|
64
|
+
* @param argv Arguments after the `init` subcommand name.
|
|
65
|
+
* @returns Parsed flags with defaults applied.
|
|
66
|
+
*/
|
|
67
|
+
function parseArgs(argv: string[]): InitCli {
|
|
68
|
+
const cli: InitCli = { fix: false, force: false, dryRun: false };
|
|
69
|
+
for (const a of argv) {
|
|
70
|
+
if (a === '--fix') cli.fix = true;
|
|
71
|
+
else if (a === '--force') cli.force = true;
|
|
72
|
+
else if (a === '--dry-run') cli.dryRun = true;
|
|
73
|
+
else {
|
|
74
|
+
console.error(`Unknown init option: ${a}`);
|
|
75
|
+
process.exit(2);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return cli;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Run a git command and capture its output.
|
|
83
|
+
*
|
|
84
|
+
* @param args Arguments passed to `git`.
|
|
85
|
+
* @returns Trimmed stdout, or `null` when git exits non-zero (unset config, not a repo, …).
|
|
86
|
+
*/
|
|
87
|
+
function git(args: string[]): string | null {
|
|
88
|
+
try {
|
|
89
|
+
return execFileSync('git', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
90
|
+
} catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Print an error to stderr and exit with status 1.
|
|
97
|
+
*
|
|
98
|
+
* @param message Error text explaining why init refused to proceed.
|
|
99
|
+
*/
|
|
100
|
+
function fail(message: string): never {
|
|
101
|
+
console.error(message);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Run the `init` subcommand: install the pre-commit hook and point `core.hooksPath` at it.
|
|
107
|
+
*
|
|
108
|
+
* @param argv Arguments after the `init` subcommand name.
|
|
109
|
+
*/
|
|
110
|
+
export async function runInit(argv: string[]): Promise<void> {
|
|
111
|
+
const cli = parseArgs(argv);
|
|
112
|
+
const hookBody = cli.fix ? HOOK_FIX : HOOK_CHECK;
|
|
113
|
+
const variant = cli.fix ? 'fix' : 'check';
|
|
114
|
+
|
|
115
|
+
// Anchor everything at the repository root: a relative `core.hooksPath` resolves there,
|
|
116
|
+
// and the hook's `./node_modules/...` path assumes hooks run from it (they do, for pre-commit).
|
|
117
|
+
const top = git(['rev-parse', '--show-toplevel']);
|
|
118
|
+
if (top === null) fail('Not a git repository (or a bare one) — run init from inside a working tree.');
|
|
119
|
+
const gitDir = git(['rev-parse', '--absolute-git-dir'])!;
|
|
120
|
+
const hookAbs = join(top, HOOK_PATH);
|
|
121
|
+
|
|
122
|
+
// Decide whether `core.hooksPath` needs to change. Repointing it makes git ignore `.git/hooks` entirely,
|
|
123
|
+
// so refuse to silently disable hooks that already live there.
|
|
124
|
+
const hooksPath = git(['config', '--get', 'core.hooksPath']);
|
|
125
|
+
let setConfig = false;
|
|
126
|
+
if (hooksPath === null) {
|
|
127
|
+
const live = (await readdir(join(gitDir, 'hooks')).catch(() => [])).filter((f) => !f.endsWith('.sample'));
|
|
128
|
+
if (live.length > 0 && !cli.force) {
|
|
129
|
+
fail(
|
|
130
|
+
`Found existing hook(s) in .git/hooks: ${live.join(', ')}.\n` +
|
|
131
|
+
`Setting core.hooksPath would disable them. Move them into ${HOOKS_DIR}/ first, ` +
|
|
132
|
+
'or re-run with --force to proceed anyway.',
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
setConfig = true;
|
|
136
|
+
} else if (hooksPath !== HOOKS_DIR) {
|
|
137
|
+
if (!cli.force) {
|
|
138
|
+
fail(
|
|
139
|
+
`core.hooksPath is already set to "${hooksPath}" — add the hook there yourself, or re-run ` +
|
|
140
|
+
`with --force to repoint it to ${HOOKS_DIR}. Hook body:\n\n${hookBody}`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
setConfig = true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Decide whether the hook file needs writing; overwriting a differing hook needs --force.
|
|
147
|
+
const current = await readFile(hookAbs, 'utf8').catch(() => null);
|
|
148
|
+
let writeHook = current === null;
|
|
149
|
+
let repairMode = false;
|
|
150
|
+
if (current !== null && current !== hookBody) {
|
|
151
|
+
if (!cli.force) {
|
|
152
|
+
const installed =
|
|
153
|
+
current === HOOK_CHECK ? 'the check variant' : current === HOOK_FIX ? 'the --fix variant' : 'a custom hook';
|
|
154
|
+
fail(`${HOOK_PATH} already exists and differs (${installed} is installed). Re-run with --force to overwrite.`);
|
|
155
|
+
}
|
|
156
|
+
writeHook = true;
|
|
157
|
+
}
|
|
158
|
+
if (current === hookBody) {
|
|
159
|
+
// Content is already right; still repair a missing executable bit, or git silently skips the hook.
|
|
160
|
+
repairMode = ((await stat(hookAbs)).mode & 0o111) === 0;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const done: string[] = [];
|
|
164
|
+
if (writeHook) {
|
|
165
|
+
if (!cli.dryRun) {
|
|
166
|
+
await mkdir(join(top, HOOKS_DIR), { recursive: true });
|
|
167
|
+
await writeFile(hookAbs, hookBody);
|
|
168
|
+
await chmod(hookAbs, 0o755);
|
|
169
|
+
}
|
|
170
|
+
done.push(
|
|
171
|
+
cli.dryRun ? `would install ${HOOK_PATH} (${variant} variant)` : `installed ${HOOK_PATH} (${variant} variant)`,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
if (repairMode) {
|
|
175
|
+
if (!cli.dryRun) await chmod(hookAbs, 0o755);
|
|
176
|
+
done.push(cli.dryRun ? `would make ${HOOK_PATH} executable` : `made ${HOOK_PATH} executable`);
|
|
177
|
+
}
|
|
178
|
+
if (setConfig) {
|
|
179
|
+
if (!cli.dryRun) git(['config', 'core.hooksPath', HOOKS_DIR]);
|
|
180
|
+
done.push(cli.dryRun ? `would set core.hooksPath = ${HOOKS_DIR}` : `set core.hooksPath = ${HOOKS_DIR}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (done.length === 0) {
|
|
184
|
+
console.log(`Nothing to do — ${HOOK_PATH} (${variant} variant) is already installed.`);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
for (const line of done) console.log(line);
|
|
188
|
+
}
|