Skip to content

Commit 88bf76d

Browse files
committed
feat: improve llm support and auto discover
- add semantic release - update dependencies
1 parent 2424f4e commit 88bf76d

File tree

12 files changed

+1840
-499
lines changed

12 files changed

+1840
-499
lines changed

.github/workflows/release.yml

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,36 @@ name: Release
22

33
on:
44
push:
5-
tags:
6-
- "v*"
5+
branches: [main]
6+
workflow_dispatch:
7+
8+
concurrency:
9+
group: release
10+
cancel-in-progress: false
711

812
jobs:
913
release:
10-
name: Release
14+
name: Semantic Release
1115
runs-on: ubuntu-latest
1216
permissions:
1317
contents: write
1418
id-token: write
19+
issues: write
20+
pull-requests: write
1521

1622
steps:
1723
- name: Checkout code
1824
uses: actions/checkout@v4
25+
with:
26+
fetch-depth: 0
1927

2028
- name: Setup Node.js
2129
uses: actions/setup-node@v4
2230
with:
23-
node-version: "24"
24-
registry-url: "https://registry.npmjs.org"
31+
node-version: "22"
32+
33+
- name: Ensure npm version for trusted publishing
34+
run: npm install -g npm@latest
2535

2636
- name: Install pnpm
2737
uses: pnpm/action-setup@v4
@@ -51,14 +61,7 @@ jobs:
5161
- name: Build
5262
run: pnpm build
5363

54-
- name: Publish to npm
55-
run: pnpm publish --provenance --access public --no-git-checks
56-
env:
57-
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
58-
59-
- name: Create GitHub Release
60-
uses: softprops/action-gh-release@v2
61-
with:
62-
generate_release_notes: true
64+
- name: Semantic release
65+
run: pnpm release
6366
env:
6467
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.releaserc.cjs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
const isLocalDryRun = process.env.SEMREL_LOCAL === "1";
2+
const commitAnalyzerConfig = {
3+
releaseRules: [
4+
{ breaking: true, release: "major" },
5+
{ type: "feat", release: "minor" },
6+
{ type: "fix", release: "patch" },
7+
{ type: "perf", release: "patch" },
8+
{ type: "revert", release: "patch" },
9+
],
10+
};
11+
12+
/** @type {import('semantic-release').GlobalConfig} */
13+
module.exports = {
14+
branches: ["main"],
15+
plugins: isLocalDryRun
16+
? [["@semantic-release/commit-analyzer", commitAnalyzerConfig]]
17+
: [
18+
["@semantic-release/commit-analyzer", commitAnalyzerConfig],
19+
"@semantic-release/release-notes-generator",
20+
"@semantic-release/npm",
21+
"@semantic-release/github",
22+
],
23+
};

README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ tailwind-lint --verbose
6565
- `-c, --config <path>` - Path to Tailwind config file (default: auto-discover)
6666
- `-a, --auto` - Auto-discover files from config content patterns (legacy, enabled by default)
6767
- `--fix` - Automatically fix problems that can be fixed
68+
- `--format <text|json>` - Output format (`text` default, `json` for machine-readable output)
6869
- `-v, --verbose` - Enable verbose logging for debugging
6970
- `-h, --help` - Show help message
7071
- `--version` - Show version number
@@ -83,8 +84,33 @@ tailwind-lint "**/*.vue" --fix
8384

8485
# Lint with a specific CSS config (v4)
8586
tailwind-lint --config ./styles/app.css
87+
88+
# Machine-readable output for LLMs/agents
89+
tailwind-lint --auto --format json
8690
```
8791

92+
## LLM / Agent Integration
93+
94+
Use JSON output to avoid brittle text parsing:
95+
96+
```bash
97+
tailwind-lint --auto --format json
98+
```
99+
100+
The JSON payload includes:
101+
102+
- `ok` - `true` when no errors are found
103+
- `summary` - counts for `errors`, `warnings`, `fixed`, `filesWithIssues`, `totalFilesProcessed`
104+
- `config` - resolved runtime values (`cwd`, `configPath`, `autoDiscover`, `fix`, `patterns`)
105+
- `files[]` - per-file diagnostics with 1-based `line`/`column` ranges
106+
107+
Typical agent flow:
108+
109+
1. Run `tailwind-lint --auto --format json`.
110+
2. If `summary.errors > 0`, fail the check and surface diagnostics.
111+
3. If only warnings exist, optionally continue and open a cleanup task.
112+
4. Re-run with `--fix` when autofix is allowed.
113+
88114
## Configuration
89115

90116
### Tailwind CSS v4
@@ -172,4 +198,25 @@ pnpm format
172198

173199
# Check code without fixing
174200
pnpm lint
201+
202+
# Preview next version locally (no publish)
203+
pnpm release:dry
175204
```
205+
206+
## Releases
207+
208+
Releases are automated with Semantic Release on pushes to `main`.
209+
210+
- Version bump is derived from Conventional Commits.
211+
- npm release and GitHub release are generated automatically.
212+
- npm publish uses npm Trusted Publishing (OIDC), no `NPM_TOKEN` required.
213+
214+
Commit examples:
215+
216+
- `feat: add json output mode` -> minor release
217+
- `fix: resolve v4 config discovery in monorepos` -> patch release
218+
- `feat: drop Node 20 support` + commit body `BREAKING CHANGE: Node 20 is no longer supported` -> major release
219+
- `perf: speed up config discovery` -> patch release
220+
- `docs: update readme` -> no release
221+
222+
`pnpm release:dry` runs against the local repo metadata (`--repository-url .`) so it does not require GitHub remote access.

__tests__/discovery.test.ts

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ import os from "node:os";
33
import path from "node:path";
44
import { afterEach, beforeEach, describe, expect, it } from "vitest";
55
import { TAILWIND_V4_IMPORT_REGEX } from "../src/constants";
6-
import { extractSourcePatterns } from "../src/linter";
6+
import {
7+
extractImportSourceDirectives,
8+
extractSourcePatterns,
9+
} from "../src/linter";
10+
import { findTailwindConfigPath } from "../src/utils/config";
711
import { readGitignorePatterns } from "../src/utils/fs";
812

913
describe("extractSourcePatterns", () => {
@@ -97,6 +101,28 @@ describe("extractSourcePatterns", () => {
97101
});
98102
});
99103

104+
describe("extractImportSourceDirectives", () => {
105+
it("should extract source roots from @import directives", () => {
106+
const css = `
107+
@import "tailwindcss" source("../src");
108+
@import "tailwindcss" source("./components");
109+
`;
110+
111+
expect(extractImportSourceDirectives(css)).toEqual({
112+
roots: ["../src", "./components"],
113+
disableAutoSource: false,
114+
});
115+
});
116+
117+
it("should detect source(none)", () => {
118+
const css = `@import "tailwindcss" source(none);`;
119+
expect(extractImportSourceDirectives(css)).toEqual({
120+
roots: [],
121+
disableAutoSource: true,
122+
});
123+
});
124+
});
125+
100126
describe("TAILWIND_V4_IMPORT_REGEX", () => {
101127
it("should match standard @import tailwindcss", () => {
102128
expect(TAILWIND_V4_IMPORT_REGEX.test('@import "tailwindcss"')).toBe(true);
@@ -222,3 +248,39 @@ describe("readGitignorePatterns", () => {
222248
expect(patterns).toEqual(["vendor/**"]);
223249
});
224250
});
251+
252+
describe("findTailwindConfigPath", () => {
253+
let tmpDir: string;
254+
255+
beforeEach(() => {
256+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "tailwind-config-test-"));
257+
});
258+
259+
afterEach(() => {
260+
fs.rmSync(tmpDir, { recursive: true, force: true });
261+
});
262+
263+
it("should discover nested v3 config files recursively", async () => {
264+
const nestedDir = path.join(tmpDir, "packages", "web");
265+
fs.mkdirSync(nestedDir, { recursive: true });
266+
fs.writeFileSync(
267+
path.join(nestedDir, "tailwind.config.js"),
268+
"module.exports = { content: ['./src/**/*.tsx'] }",
269+
);
270+
271+
const discovered = await findTailwindConfigPath(tmpDir);
272+
expect(discovered).toBe(path.join(nestedDir, "tailwind.config.js"));
273+
});
274+
275+
it("should discover nested v4 css configs recursively", async () => {
276+
const nestedDir = path.join(tmpDir, "apps", "site", "styles");
277+
fs.mkdirSync(nestedDir, { recursive: true });
278+
fs.writeFileSync(
279+
path.join(nestedDir, "theme.css"),
280+
'@import "tailwindcss";',
281+
);
282+
283+
const discovered = await findTailwindConfigPath(tmpDir);
284+
expect(discovered).toBe(path.join(nestedDir, "theme.css"));
285+
});
286+
});

__tests__/output.test.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { describe, expect, it } from "vitest";
2+
import { createJsonOutput, toJsonDiagnostic } from "../src/output";
3+
import type { Diagnostic } from "vscode-languageserver";
4+
5+
describe("toJsonDiagnostic", () => {
6+
it("should convert diagnostic ranges to 1-based positions", () => {
7+
const diagnostic: Diagnostic = {
8+
range: {
9+
start: { line: 0, character: 4 },
10+
end: { line: 0, character: 8 },
11+
},
12+
severity: 1,
13+
message: "Test error",
14+
code: "testRule",
15+
source: "tailwindcss",
16+
};
17+
18+
expect(toJsonDiagnostic(diagnostic)).toEqual({
19+
line: 1,
20+
column: 5,
21+
endLine: 1,
22+
endColumn: 9,
23+
severity: "error",
24+
code: "testRule",
25+
message: "Test error",
26+
source: "tailwindcss",
27+
});
28+
});
29+
});
30+
31+
describe("createJsonOutput", () => {
32+
it("should build machine-readable summary and config metadata", () => {
33+
const output = createJsonOutput({
34+
files: [
35+
{
36+
path: "src/example.html",
37+
fixed: true,
38+
fixedCount: 2,
39+
diagnostics: [
40+
{
41+
range: {
42+
start: { line: 0, character: 0 },
43+
end: { line: 0, character: 5 },
44+
},
45+
severity: 1,
46+
message: "Invalid class",
47+
code: "invalidClass",
48+
},
49+
{
50+
range: {
51+
start: { line: 1, character: 2 },
52+
end: { line: 1, character: 6 },
53+
},
54+
severity: 2,
55+
message: "Suggested order",
56+
code: "recommendedVariantOrder",
57+
},
58+
],
59+
},
60+
],
61+
totalFilesProcessed: 3,
62+
cwd: "/tmp/project",
63+
configPath: "src/app.css",
64+
autoDiscover: true,
65+
fix: true,
66+
patterns: [],
67+
});
68+
69+
expect(output.ok).toBe(false);
70+
expect(output.summary).toEqual({
71+
errors: 1,
72+
warnings: 1,
73+
fixed: 2,
74+
filesWithIssues: 1,
75+
totalFilesProcessed: 3,
76+
});
77+
expect(output.config).toEqual({
78+
cwd: "/tmp/project",
79+
configPath: "src/app.css",
80+
autoDiscover: true,
81+
fix: true,
82+
patterns: [],
83+
});
84+
expect(output.files[0].path).toBe("src/example.html");
85+
expect(output.files[0].diagnostics).toHaveLength(2);
86+
});
87+
});

0 commit comments

Comments
 (0)