uilint-eslint 0.1.1
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 +99 -0
- package/dist/index.d.ts +178 -0
- package/dist/index.js +748 -0
- package/dist/index.js.map +1 -0
- package/package.json +68 -0
package/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# uilint-eslint
|
|
2
|
+
|
|
3
|
+
ESLint plugin for UILint - AI-powered UI consistency checking.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add -D uilint-eslint
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
Add to your ESLint flat config:
|
|
14
|
+
|
|
15
|
+
```javascript
|
|
16
|
+
// eslint.config.js
|
|
17
|
+
import uilint from 'uilint-eslint';
|
|
18
|
+
|
|
19
|
+
export default [
|
|
20
|
+
// Use recommended preset (static rules only)
|
|
21
|
+
uilint.configs.recommended,
|
|
22
|
+
|
|
23
|
+
// Or strict preset (includes LLM-powered semantic rule)
|
|
24
|
+
// uilint.configs.strict,
|
|
25
|
+
|
|
26
|
+
// Or configure rules individually
|
|
27
|
+
{
|
|
28
|
+
plugins: { uilint: uilint.plugin },
|
|
29
|
+
rules: {
|
|
30
|
+
'uilint/no-arbitrary-tailwind': 'error',
|
|
31
|
+
'uilint/consistent-spacing': ['warn', { scale: [0, 1, 2, 3, 4, 5, 6, 8, 10, 12, 16] }],
|
|
32
|
+
'uilint/no-direct-store-import': ['error', { storePattern: 'use*Store' }],
|
|
33
|
+
'uilint/no-mixed-component-libraries': ['error', { libraries: ['shadcn', 'mui'] }],
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Rules
|
|
40
|
+
|
|
41
|
+
### Static Rules
|
|
42
|
+
|
|
43
|
+
| Rule | Description |
|
|
44
|
+
|------|-------------|
|
|
45
|
+
| `no-arbitrary-tailwind` | Forbid arbitrary Tailwind values like `w-[123px]` |
|
|
46
|
+
| `consistent-spacing` | Enforce spacing scale (no magic numbers in gap/padding) |
|
|
47
|
+
| `no-direct-store-import` | Forbid direct Zustand store imports (use hooks) |
|
|
48
|
+
| `no-mixed-component-libraries` | Forbid mixing shadcn and MUI components in same file |
|
|
49
|
+
|
|
50
|
+
### LLM-Powered Rules
|
|
51
|
+
|
|
52
|
+
| Rule | Description |
|
|
53
|
+
|------|-------------|
|
|
54
|
+
| `semantic` | LLM-powered semantic UI analysis using your styleguide |
|
|
55
|
+
|
|
56
|
+
The `semantic` rule reads `.uilint/styleguide.md` and uses Ollama to analyze your UI code for consistency violations.
|
|
57
|
+
|
|
58
|
+
## Configuration
|
|
59
|
+
|
|
60
|
+
### `no-arbitrary-tailwind`
|
|
61
|
+
|
|
62
|
+
No options. Reports any use of Tailwind arbitrary values like `w-[100px]`, `bg-[#fff]`, etc.
|
|
63
|
+
|
|
64
|
+
### `consistent-spacing`
|
|
65
|
+
|
|
66
|
+
```javascript
|
|
67
|
+
{
|
|
68
|
+
scale: number[] // Allowed spacing values (default: Tailwind defaults)
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### `no-direct-store-import`
|
|
73
|
+
|
|
74
|
+
```javascript
|
|
75
|
+
{
|
|
76
|
+
storePattern: string // Glob pattern for store names (default: 'use*Store')
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### `no-mixed-component-libraries`
|
|
81
|
+
|
|
82
|
+
```javascript
|
|
83
|
+
{
|
|
84
|
+
libraries: ('shadcn' | 'mui')[] // Libraries to check (default: ['shadcn', 'mui'])
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### `semantic`
|
|
89
|
+
|
|
90
|
+
```javascript
|
|
91
|
+
{
|
|
92
|
+
model: string // Ollama model (default: 'qwen3:8b')
|
|
93
|
+
styleguidePath: string // Path to styleguide (default: '.uilint/styleguide.md')
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## License
|
|
98
|
+
|
|
99
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import * as _typescript_eslint_utils_ts_eslint from '@typescript-eslint/utils/ts-eslint';
|
|
2
|
+
import { Linter } from 'eslint';
|
|
3
|
+
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Rule creation helper using @typescript-eslint/utils
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
declare const createRule: <Options extends readonly unknown[], MessageIds extends string>({ meta, name, ...rule }: Readonly<ESLintUtils.RuleWithMetaAndName<Options, MessageIds, unknown>>) => ESLintUtils.RuleModule<MessageIds, Options, unknown, ESLintUtils.RuleListener> & {
|
|
10
|
+
name: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Styleguide loader for the LLM semantic rule
|
|
15
|
+
*
|
|
16
|
+
* Only the semantic rule reads the styleguide - static rules use ESLint options.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Find the styleguide file path
|
|
20
|
+
*/
|
|
21
|
+
declare function findStyleguidePath(startDir: string, explicitPath?: string): string | null;
|
|
22
|
+
/**
|
|
23
|
+
* Load styleguide content from file
|
|
24
|
+
*/
|
|
25
|
+
declare function loadStyleguide(startDir: string, explicitPath?: string): string | null;
|
|
26
|
+
/**
|
|
27
|
+
* Get styleguide path and content
|
|
28
|
+
*/
|
|
29
|
+
declare function getStyleguide(startDir: string, explicitPath?: string): {
|
|
30
|
+
path: string | null;
|
|
31
|
+
content: string | null;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* File-hash based caching for LLM semantic rule
|
|
36
|
+
*
|
|
37
|
+
* Uses xxhash for fast hashing of file contents.
|
|
38
|
+
*/
|
|
39
|
+
/**
|
|
40
|
+
* Hash content using xxhash (async) or djb2 (sync fallback)
|
|
41
|
+
*/
|
|
42
|
+
declare function hashContent(content: string): Promise<string>;
|
|
43
|
+
/**
|
|
44
|
+
* Synchronous hash for when async is not possible
|
|
45
|
+
*/
|
|
46
|
+
declare function hashContentSync(content: string): string;
|
|
47
|
+
interface CacheEntry {
|
|
48
|
+
fileHash: string;
|
|
49
|
+
styleguideHash: string;
|
|
50
|
+
issues: CachedIssue[];
|
|
51
|
+
timestamp: number;
|
|
52
|
+
}
|
|
53
|
+
interface CachedIssue {
|
|
54
|
+
line: number;
|
|
55
|
+
column?: number;
|
|
56
|
+
message: string;
|
|
57
|
+
ruleId: string;
|
|
58
|
+
severity: 1 | 2;
|
|
59
|
+
}
|
|
60
|
+
interface CacheStore {
|
|
61
|
+
version: number;
|
|
62
|
+
entries: Record<string, CacheEntry>;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Load the cache store
|
|
66
|
+
*/
|
|
67
|
+
declare function loadCache(projectRoot: string): CacheStore;
|
|
68
|
+
/**
|
|
69
|
+
* Save the cache store
|
|
70
|
+
*/
|
|
71
|
+
declare function saveCache(projectRoot: string, cache: CacheStore): void;
|
|
72
|
+
/**
|
|
73
|
+
* Get cached entry for a file
|
|
74
|
+
*/
|
|
75
|
+
declare function getCacheEntry(projectRoot: string, filePath: string, fileHash: string, styleguideHash: string): CacheEntry | null;
|
|
76
|
+
/**
|
|
77
|
+
* Set cached entry for a file
|
|
78
|
+
*/
|
|
79
|
+
declare function setCacheEntry(projectRoot: string, filePath: string, entry: CacheEntry): void;
|
|
80
|
+
/**
|
|
81
|
+
* Clear cache for a specific file
|
|
82
|
+
*/
|
|
83
|
+
declare function clearCacheEntry(projectRoot: string, filePath: string): void;
|
|
84
|
+
/**
|
|
85
|
+
* Clear entire cache
|
|
86
|
+
*/
|
|
87
|
+
declare function clearCache(projectRoot: string): void;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* All available rules
|
|
91
|
+
*/
|
|
92
|
+
declare const rules: {
|
|
93
|
+
"no-arbitrary-tailwind": _typescript_eslint_utils_ts_eslint.RuleModule<"noArbitraryValue", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
|
|
94
|
+
name: string;
|
|
95
|
+
};
|
|
96
|
+
"consistent-spacing": _typescript_eslint_utils_ts_eslint.RuleModule<"invalidSpacing", [{
|
|
97
|
+
scale?: number[];
|
|
98
|
+
}], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
|
|
99
|
+
name: string;
|
|
100
|
+
};
|
|
101
|
+
"no-direct-store-import": _typescript_eslint_utils_ts_eslint.RuleModule<"noDirectImport", [{
|
|
102
|
+
storePattern?: string;
|
|
103
|
+
}], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
|
|
104
|
+
name: string;
|
|
105
|
+
};
|
|
106
|
+
"no-mixed-component-libraries": _typescript_eslint_utils_ts_eslint.RuleModule<"mixedLibraries", [{
|
|
107
|
+
libraries?: ("shadcn" | "mui")[];
|
|
108
|
+
}], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
|
|
109
|
+
name: string;
|
|
110
|
+
};
|
|
111
|
+
semantic: _typescript_eslint_utils_ts_eslint.RuleModule<"semanticIssue" | "styleguideNotFound" | "analysisError", [{
|
|
112
|
+
model?: string;
|
|
113
|
+
styleguidePath?: string;
|
|
114
|
+
}], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
|
|
115
|
+
name: string;
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* Plugin metadata
|
|
120
|
+
*/
|
|
121
|
+
declare const meta: {
|
|
122
|
+
name: string;
|
|
123
|
+
version: string;
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* The ESLint plugin object
|
|
127
|
+
*/
|
|
128
|
+
declare const plugin: {
|
|
129
|
+
meta: {
|
|
130
|
+
name: string;
|
|
131
|
+
version: string;
|
|
132
|
+
};
|
|
133
|
+
rules: {
|
|
134
|
+
"no-arbitrary-tailwind": _typescript_eslint_utils_ts_eslint.RuleModule<"noArbitraryValue", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
|
|
135
|
+
name: string;
|
|
136
|
+
};
|
|
137
|
+
"consistent-spacing": _typescript_eslint_utils_ts_eslint.RuleModule<"invalidSpacing", [{
|
|
138
|
+
scale?: number[];
|
|
139
|
+
}], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
|
|
140
|
+
name: string;
|
|
141
|
+
};
|
|
142
|
+
"no-direct-store-import": _typescript_eslint_utils_ts_eslint.RuleModule<"noDirectImport", [{
|
|
143
|
+
storePattern?: string;
|
|
144
|
+
}], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
|
|
145
|
+
name: string;
|
|
146
|
+
};
|
|
147
|
+
"no-mixed-component-libraries": _typescript_eslint_utils_ts_eslint.RuleModule<"mixedLibraries", [{
|
|
148
|
+
libraries?: ("shadcn" | "mui")[];
|
|
149
|
+
}], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
|
|
150
|
+
name: string;
|
|
151
|
+
};
|
|
152
|
+
semantic: _typescript_eslint_utils_ts_eslint.RuleModule<"semanticIssue" | "styleguideNotFound" | "analysisError", [{
|
|
153
|
+
model?: string;
|
|
154
|
+
styleguidePath?: string;
|
|
155
|
+
}], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
|
|
156
|
+
name: string;
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
};
|
|
160
|
+
/**
|
|
161
|
+
* Pre-configured configs
|
|
162
|
+
*/
|
|
163
|
+
declare const configs: Record<string, Linter.Config>;
|
|
164
|
+
/**
|
|
165
|
+
* UILint ESLint export interface
|
|
166
|
+
*/
|
|
167
|
+
interface UILintESLint {
|
|
168
|
+
meta: typeof meta;
|
|
169
|
+
plugin: typeof plugin;
|
|
170
|
+
rules: typeof rules;
|
|
171
|
+
configs: Record<string, Linter.Config>;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Default export for ESLint flat config
|
|
175
|
+
*/
|
|
176
|
+
declare const uilintEslint: UILintESLint;
|
|
177
|
+
|
|
178
|
+
export { type CacheEntry, type CacheStore, type CachedIssue, type UILintESLint, clearCache, clearCacheEntry, configs, createRule, uilintEslint as default, findStyleguidePath, getCacheEntry, getStyleguide, hashContent, hashContentSync, loadCache, loadStyleguide, meta, plugin, rules, saveCache, setCacheEntry };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,748 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
// src/utils/create-rule.ts
|
|
9
|
+
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
10
|
+
var createRule = ESLintUtils.RuleCreator(
|
|
11
|
+
(name) => `https://github.com/peter-suggate/uilint/blob/main/packages/uilint-eslint/docs/rules/${name}.md`
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
// src/rules/no-arbitrary-tailwind.ts
|
|
15
|
+
var ARBITRARY_VALUE_REGEX = /\b[\w-]+-\[[^\]]+\]/g;
|
|
16
|
+
var no_arbitrary_tailwind_default = createRule({
|
|
17
|
+
name: "no-arbitrary-tailwind",
|
|
18
|
+
meta: {
|
|
19
|
+
type: "problem",
|
|
20
|
+
docs: {
|
|
21
|
+
description: "Forbid arbitrary Tailwind values like w-[123px]"
|
|
22
|
+
},
|
|
23
|
+
messages: {
|
|
24
|
+
noArbitraryValue: "Avoid arbitrary Tailwind value '{{value}}'. Use the spacing/color scale instead."
|
|
25
|
+
},
|
|
26
|
+
schema: []
|
|
27
|
+
},
|
|
28
|
+
defaultOptions: [],
|
|
29
|
+
create(context) {
|
|
30
|
+
return {
|
|
31
|
+
// Check className attributes in JSX
|
|
32
|
+
JSXAttribute(node) {
|
|
33
|
+
if (node.name.type === "JSXIdentifier" && (node.name.name === "className" || node.name.name === "class")) {
|
|
34
|
+
const value = node.value;
|
|
35
|
+
if (value?.type === "Literal" && typeof value.value === "string") {
|
|
36
|
+
checkClassString(context, value, value.value);
|
|
37
|
+
}
|
|
38
|
+
if (value?.type === "JSXExpressionContainer") {
|
|
39
|
+
const expr = value.expression;
|
|
40
|
+
if (expr.type === "Literal" && typeof expr.value === "string") {
|
|
41
|
+
checkClassString(context, expr, expr.value);
|
|
42
|
+
}
|
|
43
|
+
if (expr.type === "TemplateLiteral") {
|
|
44
|
+
for (const quasi of expr.quasis) {
|
|
45
|
+
checkClassString(context, quasi, quasi.value.raw);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
// Check cn(), clsx(), classnames() calls
|
|
52
|
+
CallExpression(node) {
|
|
53
|
+
if (node.callee.type !== "Identifier") return;
|
|
54
|
+
const name = node.callee.name;
|
|
55
|
+
if (name === "cn" || name === "clsx" || name === "classnames") {
|
|
56
|
+
for (const arg of node.arguments) {
|
|
57
|
+
if (arg.type === "Literal" && typeof arg.value === "string") {
|
|
58
|
+
checkClassString(context, arg, arg.value);
|
|
59
|
+
}
|
|
60
|
+
if (arg.type === "TemplateLiteral") {
|
|
61
|
+
for (const quasi of arg.quasis) {
|
|
62
|
+
checkClassString(context, quasi, quasi.value.raw);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
function checkClassString(context, node, classString) {
|
|
72
|
+
const matches = classString.matchAll(ARBITRARY_VALUE_REGEX);
|
|
73
|
+
for (const match of matches) {
|
|
74
|
+
context.report({
|
|
75
|
+
node,
|
|
76
|
+
messageId: "noArbitraryValue",
|
|
77
|
+
data: { value: match[0] }
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/rules/consistent-spacing.ts
|
|
83
|
+
var DEFAULT_SCALE = [
|
|
84
|
+
0,
|
|
85
|
+
0.5,
|
|
86
|
+
1,
|
|
87
|
+
1.5,
|
|
88
|
+
2,
|
|
89
|
+
2.5,
|
|
90
|
+
3,
|
|
91
|
+
3.5,
|
|
92
|
+
4,
|
|
93
|
+
5,
|
|
94
|
+
6,
|
|
95
|
+
7,
|
|
96
|
+
8,
|
|
97
|
+
9,
|
|
98
|
+
10,
|
|
99
|
+
11,
|
|
100
|
+
12,
|
|
101
|
+
14,
|
|
102
|
+
16,
|
|
103
|
+
20,
|
|
104
|
+
24,
|
|
105
|
+
28,
|
|
106
|
+
32,
|
|
107
|
+
36,
|
|
108
|
+
40,
|
|
109
|
+
44,
|
|
110
|
+
48,
|
|
111
|
+
52,
|
|
112
|
+
56,
|
|
113
|
+
60,
|
|
114
|
+
64,
|
|
115
|
+
72,
|
|
116
|
+
80,
|
|
117
|
+
96
|
|
118
|
+
];
|
|
119
|
+
var SPACING_PREFIXES = [
|
|
120
|
+
"p-",
|
|
121
|
+
"px-",
|
|
122
|
+
"py-",
|
|
123
|
+
"pt-",
|
|
124
|
+
"pr-",
|
|
125
|
+
"pb-",
|
|
126
|
+
"pl-",
|
|
127
|
+
"ps-",
|
|
128
|
+
"pe-",
|
|
129
|
+
"m-",
|
|
130
|
+
"mx-",
|
|
131
|
+
"my-",
|
|
132
|
+
"mt-",
|
|
133
|
+
"mr-",
|
|
134
|
+
"mb-",
|
|
135
|
+
"ml-",
|
|
136
|
+
"ms-",
|
|
137
|
+
"me-",
|
|
138
|
+
"gap-",
|
|
139
|
+
"gap-x-",
|
|
140
|
+
"gap-y-",
|
|
141
|
+
"space-x-",
|
|
142
|
+
"space-y-",
|
|
143
|
+
"inset-",
|
|
144
|
+
"inset-x-",
|
|
145
|
+
"inset-y-",
|
|
146
|
+
"top-",
|
|
147
|
+
"right-",
|
|
148
|
+
"bottom-",
|
|
149
|
+
"left-",
|
|
150
|
+
"w-",
|
|
151
|
+
"h-",
|
|
152
|
+
"min-w-",
|
|
153
|
+
"min-h-",
|
|
154
|
+
"max-w-",
|
|
155
|
+
"max-h-",
|
|
156
|
+
"size-"
|
|
157
|
+
];
|
|
158
|
+
function buildSpacingRegex() {
|
|
159
|
+
const prefixes = SPACING_PREFIXES.map((p) => p.replace("-", "\\-")).join("|");
|
|
160
|
+
return new RegExp(`\\b(${prefixes})(\\d+\\.?\\d*)\\b`, "g");
|
|
161
|
+
}
|
|
162
|
+
var SPACING_REGEX = buildSpacingRegex();
|
|
163
|
+
var consistent_spacing_default = createRule({
|
|
164
|
+
name: "consistent-spacing",
|
|
165
|
+
meta: {
|
|
166
|
+
type: "suggestion",
|
|
167
|
+
docs: {
|
|
168
|
+
description: "Enforce spacing scale (no magic numbers in gap/padding)"
|
|
169
|
+
},
|
|
170
|
+
messages: {
|
|
171
|
+
invalidSpacing: "Spacing value '{{value}}' is not in the allowed scale. Use one of: {{allowed}}"
|
|
172
|
+
},
|
|
173
|
+
schema: [
|
|
174
|
+
{
|
|
175
|
+
type: "object",
|
|
176
|
+
properties: {
|
|
177
|
+
scale: {
|
|
178
|
+
type: "array",
|
|
179
|
+
items: { type: "number" }
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
additionalProperties: false
|
|
183
|
+
}
|
|
184
|
+
]
|
|
185
|
+
},
|
|
186
|
+
defaultOptions: [{ scale: DEFAULT_SCALE }],
|
|
187
|
+
create(context) {
|
|
188
|
+
const options = context.options[0] || {};
|
|
189
|
+
const scale = new Set((options.scale || DEFAULT_SCALE).map(String));
|
|
190
|
+
const scaleList = [...scale].slice(0, 10).join(", ") + "...";
|
|
191
|
+
return {
|
|
192
|
+
// Check className attributes in JSX
|
|
193
|
+
JSXAttribute(node) {
|
|
194
|
+
if (node.name.type === "JSXIdentifier" && (node.name.name === "className" || node.name.name === "class")) {
|
|
195
|
+
const value = node.value;
|
|
196
|
+
if (value?.type === "Literal" && typeof value.value === "string") {
|
|
197
|
+
checkSpacing(context, node, value.value, scale, scaleList);
|
|
198
|
+
}
|
|
199
|
+
if (value?.type === "JSXExpressionContainer") {
|
|
200
|
+
const expr = value.expression;
|
|
201
|
+
if (expr.type === "Literal" && typeof expr.value === "string") {
|
|
202
|
+
checkSpacing(context, node, expr.value, scale, scaleList);
|
|
203
|
+
}
|
|
204
|
+
if (expr.type === "TemplateLiteral") {
|
|
205
|
+
for (const quasi of expr.quasis) {
|
|
206
|
+
checkSpacing(context, node, quasi.value.raw, scale, scaleList);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
// Check cn(), clsx(), classnames() calls
|
|
213
|
+
CallExpression(node) {
|
|
214
|
+
if (node.callee.type !== "Identifier") return;
|
|
215
|
+
const name = node.callee.name;
|
|
216
|
+
if (name === "cn" || name === "clsx" || name === "classnames") {
|
|
217
|
+
for (const arg of node.arguments) {
|
|
218
|
+
if (arg.type === "Literal" && typeof arg.value === "string") {
|
|
219
|
+
checkSpacing(context, arg, arg.value, scale, scaleList);
|
|
220
|
+
}
|
|
221
|
+
if (arg.type === "TemplateLiteral") {
|
|
222
|
+
for (const quasi of arg.quasis) {
|
|
223
|
+
checkSpacing(context, quasi, quasi.value.raw, scale, scaleList);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
function checkSpacing(context, node, classString, scale, scaleList) {
|
|
233
|
+
SPACING_REGEX.lastIndex = 0;
|
|
234
|
+
let match;
|
|
235
|
+
while ((match = SPACING_REGEX.exec(classString)) !== null) {
|
|
236
|
+
const [, , value] = match;
|
|
237
|
+
if (value && !scale.has(value)) {
|
|
238
|
+
context.report({
|
|
239
|
+
node,
|
|
240
|
+
messageId: "invalidSpacing",
|
|
241
|
+
data: { value, allowed: scaleList }
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// src/rules/no-direct-store-import.ts
|
|
248
|
+
function patternToRegex(pattern) {
|
|
249
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
250
|
+
return new RegExp(`^${escaped}$`);
|
|
251
|
+
}
|
|
252
|
+
var no_direct_store_import_default = createRule({
|
|
253
|
+
name: "no-direct-store-import",
|
|
254
|
+
meta: {
|
|
255
|
+
type: "problem",
|
|
256
|
+
docs: {
|
|
257
|
+
description: "Forbid direct Zustand store imports (use hooks via context)"
|
|
258
|
+
},
|
|
259
|
+
messages: {
|
|
260
|
+
noDirectImport: "Avoid importing store '{{name}}' directly. Use the store via a context hook instead."
|
|
261
|
+
},
|
|
262
|
+
schema: [
|
|
263
|
+
{
|
|
264
|
+
type: "object",
|
|
265
|
+
properties: {
|
|
266
|
+
storePattern: {
|
|
267
|
+
type: "string",
|
|
268
|
+
description: "Glob pattern for store names"
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
additionalProperties: false
|
|
272
|
+
}
|
|
273
|
+
]
|
|
274
|
+
},
|
|
275
|
+
defaultOptions: [{ storePattern: "use*Store" }],
|
|
276
|
+
create(context) {
|
|
277
|
+
const options = context.options[0] || {};
|
|
278
|
+
const pattern = options.storePattern || "use*Store";
|
|
279
|
+
const regex = patternToRegex(pattern);
|
|
280
|
+
return {
|
|
281
|
+
ImportDeclaration(node) {
|
|
282
|
+
const source = node.source.value;
|
|
283
|
+
if (!source.includes("store")) return;
|
|
284
|
+
for (const specifier of node.specifiers) {
|
|
285
|
+
if (specifier.type === "ImportSpecifier") {
|
|
286
|
+
const importedName = specifier.imported.type === "Identifier" ? specifier.imported.name : specifier.imported.value;
|
|
287
|
+
if (regex.test(importedName)) {
|
|
288
|
+
context.report({
|
|
289
|
+
node: specifier,
|
|
290
|
+
messageId: "noDirectImport",
|
|
291
|
+
data: { name: importedName }
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (specifier.type === "ImportDefaultSpecifier") {
|
|
296
|
+
const localName = specifier.local.name;
|
|
297
|
+
if (regex.test(localName)) {
|
|
298
|
+
context.report({
|
|
299
|
+
node: specifier,
|
|
300
|
+
messageId: "noDirectImport",
|
|
301
|
+
data: { name: localName }
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// src/rules/no-mixed-component-libraries.ts
|
|
312
|
+
var LIBRARY_PATTERNS = {
|
|
313
|
+
shadcn: ["@/components/ui", "@radix-ui/", "components/ui/"],
|
|
314
|
+
mui: ["@mui/material", "@mui/icons-material", "@emotion/"]
|
|
315
|
+
};
|
|
316
|
+
var no_mixed_component_libraries_default = createRule({
|
|
317
|
+
name: "no-mixed-component-libraries",
|
|
318
|
+
meta: {
|
|
319
|
+
type: "problem",
|
|
320
|
+
docs: {
|
|
321
|
+
description: "Forbid mixing component libraries in the same file"
|
|
322
|
+
},
|
|
323
|
+
messages: {
|
|
324
|
+
mixedLibraries: "Mixing {{lib1}} and {{lib2}} components. Choose one library per file."
|
|
325
|
+
},
|
|
326
|
+
schema: [
|
|
327
|
+
{
|
|
328
|
+
type: "object",
|
|
329
|
+
properties: {
|
|
330
|
+
libraries: {
|
|
331
|
+
type: "array",
|
|
332
|
+
items: { enum: ["shadcn", "mui"] }
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
additionalProperties: false
|
|
336
|
+
}
|
|
337
|
+
]
|
|
338
|
+
},
|
|
339
|
+
defaultOptions: [{ libraries: ["shadcn", "mui"] }],
|
|
340
|
+
create(context) {
|
|
341
|
+
const options = context.options[0] || {};
|
|
342
|
+
const libraries = options.libraries || ["shadcn", "mui"];
|
|
343
|
+
const detected = /* @__PURE__ */ new Map();
|
|
344
|
+
return {
|
|
345
|
+
ImportDeclaration(node) {
|
|
346
|
+
const source = node.source.value;
|
|
347
|
+
for (const lib of libraries) {
|
|
348
|
+
const patterns = LIBRARY_PATTERNS[lib];
|
|
349
|
+
if (patterns?.some((p) => source.includes(p))) {
|
|
350
|
+
if (!detected.has(lib)) {
|
|
351
|
+
detected.set(lib, node);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
},
|
|
356
|
+
"Program:exit"() {
|
|
357
|
+
if (detected.size > 1) {
|
|
358
|
+
const libs = [...detected.keys()];
|
|
359
|
+
const secondLib = libs[1];
|
|
360
|
+
const secondNode = detected.get(secondLib);
|
|
361
|
+
context.report({
|
|
362
|
+
node: secondNode,
|
|
363
|
+
messageId: "mixedLibraries",
|
|
364
|
+
data: { lib1: libs[0], lib2: secondLib }
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// src/rules/semantic.ts
|
|
373
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
374
|
+
import { dirname as dirname3, relative } from "path";
|
|
375
|
+
|
|
376
|
+
// src/utils/cache.ts
|
|
377
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
378
|
+
import { dirname, join } from "path";
|
|
379
|
+
var xxhashInstance = null;
|
|
380
|
+
async function getXxhash() {
|
|
381
|
+
if (!xxhashInstance) {
|
|
382
|
+
const xxhash = await import("xxhash-wasm");
|
|
383
|
+
xxhashInstance = await xxhash.default();
|
|
384
|
+
}
|
|
385
|
+
return xxhashInstance;
|
|
386
|
+
}
|
|
387
|
+
function djb2Hash(str) {
|
|
388
|
+
let hash = 5381;
|
|
389
|
+
for (let i = 0; i < str.length; i++) {
|
|
390
|
+
hash = hash * 33 ^ str.charCodeAt(i);
|
|
391
|
+
}
|
|
392
|
+
return (hash >>> 0).toString(16);
|
|
393
|
+
}
|
|
394
|
+
async function hashContent(content) {
|
|
395
|
+
try {
|
|
396
|
+
const xxhash = await getXxhash();
|
|
397
|
+
return xxhash.h64ToString(content);
|
|
398
|
+
} catch {
|
|
399
|
+
return djb2Hash(content);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
function hashContentSync(content) {
|
|
403
|
+
return djb2Hash(content);
|
|
404
|
+
}
|
|
405
|
+
var CACHE_VERSION = 1;
|
|
406
|
+
var CACHE_FILE = ".uilint/.cache/eslint-semantic.json";
|
|
407
|
+
function getCachePath(projectRoot) {
|
|
408
|
+
return join(projectRoot, CACHE_FILE);
|
|
409
|
+
}
|
|
410
|
+
function loadCache(projectRoot) {
|
|
411
|
+
const cachePath = getCachePath(projectRoot);
|
|
412
|
+
if (!existsSync(cachePath)) {
|
|
413
|
+
return { version: CACHE_VERSION, entries: {} };
|
|
414
|
+
}
|
|
415
|
+
try {
|
|
416
|
+
const content = readFileSync(cachePath, "utf-8");
|
|
417
|
+
const cache = JSON.parse(content);
|
|
418
|
+
if (cache.version !== CACHE_VERSION) {
|
|
419
|
+
return { version: CACHE_VERSION, entries: {} };
|
|
420
|
+
}
|
|
421
|
+
return cache;
|
|
422
|
+
} catch {
|
|
423
|
+
return { version: CACHE_VERSION, entries: {} };
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
function saveCache(projectRoot, cache) {
|
|
427
|
+
const cachePath = getCachePath(projectRoot);
|
|
428
|
+
try {
|
|
429
|
+
const cacheDir = dirname(cachePath);
|
|
430
|
+
if (!existsSync(cacheDir)) {
|
|
431
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
432
|
+
}
|
|
433
|
+
writeFileSync(cachePath, JSON.stringify(cache, null, 2), "utf-8");
|
|
434
|
+
} catch {
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
function getCacheEntry(projectRoot, filePath, fileHash, styleguideHash) {
|
|
438
|
+
const cache = loadCache(projectRoot);
|
|
439
|
+
const entry = cache.entries[filePath];
|
|
440
|
+
if (!entry) return null;
|
|
441
|
+
if (entry.fileHash !== fileHash || entry.styleguideHash !== styleguideHash) {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
return entry;
|
|
445
|
+
}
|
|
446
|
+
function setCacheEntry(projectRoot, filePath, entry) {
|
|
447
|
+
const cache = loadCache(projectRoot);
|
|
448
|
+
cache.entries[filePath] = entry;
|
|
449
|
+
saveCache(projectRoot, cache);
|
|
450
|
+
}
|
|
451
|
+
function clearCacheEntry(projectRoot, filePath) {
|
|
452
|
+
const cache = loadCache(projectRoot);
|
|
453
|
+
delete cache.entries[filePath];
|
|
454
|
+
saveCache(projectRoot, cache);
|
|
455
|
+
}
|
|
456
|
+
function clearCache(projectRoot) {
|
|
457
|
+
saveCache(projectRoot, { version: CACHE_VERSION, entries: {} });
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// src/utils/styleguide-loader.ts
|
|
461
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
462
|
+
import { dirname as dirname2, isAbsolute, join as join2, resolve } from "path";
|
|
463
|
+
var DEFAULT_STYLEGUIDE_PATHS = [
|
|
464
|
+
".uilint/styleguide.md",
|
|
465
|
+
".uilint/styleguide.yaml",
|
|
466
|
+
".uilint/styleguide.yml"
|
|
467
|
+
];
|
|
468
|
+
function findWorkspaceRoot(startDir) {
|
|
469
|
+
let dir = startDir;
|
|
470
|
+
for (let i = 0; i < 20; i++) {
|
|
471
|
+
if (existsSync2(join2(dir, "pnpm-workspace.yaml")) || existsSync2(join2(dir, ".git"))) {
|
|
472
|
+
return dir;
|
|
473
|
+
}
|
|
474
|
+
const parent = dirname2(dir);
|
|
475
|
+
if (parent === dir) break;
|
|
476
|
+
dir = parent;
|
|
477
|
+
}
|
|
478
|
+
return startDir;
|
|
479
|
+
}
|
|
480
|
+
function findStyleguidePath(startDir, explicitPath) {
|
|
481
|
+
if (explicitPath) {
|
|
482
|
+
const resolved = isAbsolute(explicitPath) ? explicitPath : resolve(startDir, explicitPath);
|
|
483
|
+
if (existsSync2(resolved)) {
|
|
484
|
+
return resolved;
|
|
485
|
+
}
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
const workspaceRoot = findWorkspaceRoot(startDir);
|
|
489
|
+
let dir = startDir;
|
|
490
|
+
while (true) {
|
|
491
|
+
for (const relativePath of DEFAULT_STYLEGUIDE_PATHS) {
|
|
492
|
+
const fullPath = join2(dir, relativePath);
|
|
493
|
+
if (existsSync2(fullPath)) {
|
|
494
|
+
return fullPath;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if (dir === workspaceRoot) break;
|
|
498
|
+
const parent = dirname2(dir);
|
|
499
|
+
if (parent === dir) break;
|
|
500
|
+
dir = parent;
|
|
501
|
+
}
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
function loadStyleguide(startDir, explicitPath) {
|
|
505
|
+
const path = findStyleguidePath(startDir, explicitPath);
|
|
506
|
+
if (!path) return null;
|
|
507
|
+
try {
|
|
508
|
+
return readFileSync2(path, "utf-8");
|
|
509
|
+
} catch {
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
function getStyleguide(startDir, explicitPath) {
|
|
514
|
+
const path = findStyleguidePath(startDir, explicitPath);
|
|
515
|
+
if (!path) return { path: null, content: null };
|
|
516
|
+
try {
|
|
517
|
+
const content = readFileSync2(path, "utf-8");
|
|
518
|
+
return { path, content };
|
|
519
|
+
} catch {
|
|
520
|
+
return { path, content: null };
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// src/rules/semantic.ts
|
|
525
|
+
var pendingAnalysis = /* @__PURE__ */ new Map();
|
|
526
|
+
var semantic_default = createRule({
|
|
527
|
+
name: "semantic",
|
|
528
|
+
meta: {
|
|
529
|
+
type: "suggestion",
|
|
530
|
+
docs: {
|
|
531
|
+
description: "LLM-powered semantic UI analysis using styleguide"
|
|
532
|
+
},
|
|
533
|
+
messages: {
|
|
534
|
+
semanticIssue: "{{message}}",
|
|
535
|
+
styleguideNotFound: "No styleguide found. Create .uilint/styleguide.md or specify styleguidePath.",
|
|
536
|
+
analysisError: "Semantic analysis failed: {{error}}"
|
|
537
|
+
},
|
|
538
|
+
schema: [
|
|
539
|
+
{
|
|
540
|
+
type: "object",
|
|
541
|
+
properties: {
|
|
542
|
+
model: {
|
|
543
|
+
type: "string",
|
|
544
|
+
description: "Ollama model to use"
|
|
545
|
+
},
|
|
546
|
+
styleguidePath: {
|
|
547
|
+
type: "string",
|
|
548
|
+
description: "Path to styleguide file"
|
|
549
|
+
}
|
|
550
|
+
},
|
|
551
|
+
additionalProperties: false
|
|
552
|
+
}
|
|
553
|
+
]
|
|
554
|
+
},
|
|
555
|
+
defaultOptions: [{ model: "qwen3:8b" }],
|
|
556
|
+
create(context) {
|
|
557
|
+
const options = context.options[0] || {};
|
|
558
|
+
const filePath = context.filename;
|
|
559
|
+
const fileDir = dirname3(filePath);
|
|
560
|
+
const { path: styleguidePath, content: styleguide } = getStyleguide(
|
|
561
|
+
fileDir,
|
|
562
|
+
options.styleguidePath
|
|
563
|
+
);
|
|
564
|
+
if (!styleguide) {
|
|
565
|
+
return {
|
|
566
|
+
Program(node) {
|
|
567
|
+
context.report({
|
|
568
|
+
node,
|
|
569
|
+
messageId: "styleguideNotFound"
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
let fileContent;
|
|
575
|
+
try {
|
|
576
|
+
fileContent = readFileSync3(filePath, "utf-8");
|
|
577
|
+
} catch {
|
|
578
|
+
return {};
|
|
579
|
+
}
|
|
580
|
+
const fileHash = hashContentSync(fileContent);
|
|
581
|
+
const styleguideHash = hashContentSync(styleguide);
|
|
582
|
+
const projectRoot = findProjectRoot(fileDir);
|
|
583
|
+
const relativeFilePath = relative(projectRoot, filePath);
|
|
584
|
+
const cached = getCacheEntry(
|
|
585
|
+
projectRoot,
|
|
586
|
+
relativeFilePath,
|
|
587
|
+
fileHash,
|
|
588
|
+
styleguideHash
|
|
589
|
+
);
|
|
590
|
+
if (cached) {
|
|
591
|
+
return {
|
|
592
|
+
Program(node) {
|
|
593
|
+
for (const issue of cached.issues) {
|
|
594
|
+
context.report({
|
|
595
|
+
node,
|
|
596
|
+
loc: { line: issue.line, column: issue.column || 0 },
|
|
597
|
+
messageId: "semanticIssue",
|
|
598
|
+
data: { message: issue.message }
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
if (!pendingAnalysis.has(filePath)) {
|
|
605
|
+
const analysisPromise = runSemanticAnalysis(
|
|
606
|
+
fileContent,
|
|
607
|
+
styleguide,
|
|
608
|
+
options.model || "qwen3:8b"
|
|
609
|
+
);
|
|
610
|
+
pendingAnalysis.set(filePath, analysisPromise);
|
|
611
|
+
analysisPromise.then((issues) => {
|
|
612
|
+
setCacheEntry(projectRoot, relativeFilePath, {
|
|
613
|
+
fileHash,
|
|
614
|
+
styleguideHash,
|
|
615
|
+
issues,
|
|
616
|
+
timestamp: Date.now()
|
|
617
|
+
});
|
|
618
|
+
}).catch(() => {
|
|
619
|
+
}).finally(() => {
|
|
620
|
+
pendingAnalysis.delete(filePath);
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
return {};
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
function findProjectRoot(startDir) {
|
|
627
|
+
const { existsSync: existsSync3 } = __require("fs");
|
|
628
|
+
const { dirname: dirname4, join: join3 } = __require("path");
|
|
629
|
+
let dir = startDir;
|
|
630
|
+
for (let i = 0; i < 20; i++) {
|
|
631
|
+
if (existsSync3(join3(dir, "package.json"))) {
|
|
632
|
+
return dir;
|
|
633
|
+
}
|
|
634
|
+
const parent = dirname4(dir);
|
|
635
|
+
if (parent === dir) break;
|
|
636
|
+
dir = parent;
|
|
637
|
+
}
|
|
638
|
+
return startDir;
|
|
639
|
+
}
|
|
640
|
+
async function runSemanticAnalysis(sourceCode, styleguide, model) {
|
|
641
|
+
try {
|
|
642
|
+
const { OllamaClient, buildSourceScanPrompt } = await import("uilint-core");
|
|
643
|
+
const client = new OllamaClient({ model });
|
|
644
|
+
const available = await client.isAvailable();
|
|
645
|
+
if (!available) {
|
|
646
|
+
console.warn("[uilint-eslint] Ollama not available, skipping semantic analysis");
|
|
647
|
+
return [];
|
|
648
|
+
}
|
|
649
|
+
const prompt = buildSourceScanPrompt(sourceCode, styleguide, {});
|
|
650
|
+
const response = await client.complete(prompt, { json: true });
|
|
651
|
+
const parsed = JSON.parse(response);
|
|
652
|
+
return (parsed.issues || []).map((issue) => ({
|
|
653
|
+
line: issue.line || 1,
|
|
654
|
+
column: issue.column,
|
|
655
|
+
message: issue.message || "Semantic issue detected",
|
|
656
|
+
ruleId: "uilint/semantic",
|
|
657
|
+
severity: 1
|
|
658
|
+
}));
|
|
659
|
+
} catch (error) {
|
|
660
|
+
console.error("[uilint-eslint] Semantic analysis error:", error);
|
|
661
|
+
return [];
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// src/index.ts
|
|
666
|
+
var version = "0.1.0";
|
|
667
|
+
var rules = {
|
|
668
|
+
"no-arbitrary-tailwind": no_arbitrary_tailwind_default,
|
|
669
|
+
"consistent-spacing": consistent_spacing_default,
|
|
670
|
+
"no-direct-store-import": no_direct_store_import_default,
|
|
671
|
+
"no-mixed-component-libraries": no_mixed_component_libraries_default,
|
|
672
|
+
semantic: semantic_default
|
|
673
|
+
};
|
|
674
|
+
var meta = {
|
|
675
|
+
name: "uilint",
|
|
676
|
+
version
|
|
677
|
+
};
|
|
678
|
+
var plugin = {
|
|
679
|
+
meta,
|
|
680
|
+
rules
|
|
681
|
+
};
|
|
682
|
+
var jsxLanguageOptions = {
|
|
683
|
+
parserOptions: {
|
|
684
|
+
ecmaFeatures: {
|
|
685
|
+
jsx: true
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
var recommendedConfig = {
|
|
690
|
+
name: "uilint/recommended",
|
|
691
|
+
plugins: {
|
|
692
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
693
|
+
uilint: plugin
|
|
694
|
+
},
|
|
695
|
+
languageOptions: jsxLanguageOptions,
|
|
696
|
+
rules: {
|
|
697
|
+
"uilint/no-arbitrary-tailwind": "error",
|
|
698
|
+
"uilint/consistent-spacing": "warn",
|
|
699
|
+
"uilint/no-direct-store-import": "warn",
|
|
700
|
+
"uilint/no-mixed-component-libraries": "error"
|
|
701
|
+
}
|
|
702
|
+
};
|
|
703
|
+
var strictConfig = {
|
|
704
|
+
name: "uilint/strict",
|
|
705
|
+
plugins: {
|
|
706
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
707
|
+
uilint: plugin
|
|
708
|
+
},
|
|
709
|
+
languageOptions: jsxLanguageOptions,
|
|
710
|
+
rules: {
|
|
711
|
+
"uilint/no-arbitrary-tailwind": "error",
|
|
712
|
+
"uilint/consistent-spacing": "warn",
|
|
713
|
+
"uilint/no-direct-store-import": "error",
|
|
714
|
+
"uilint/no-mixed-component-libraries": "error",
|
|
715
|
+
"uilint/semantic": "warn"
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
var configs = {
|
|
719
|
+
recommended: recommendedConfig,
|
|
720
|
+
strict: strictConfig
|
|
721
|
+
};
|
|
722
|
+
var uilintEslint = {
|
|
723
|
+
meta,
|
|
724
|
+
plugin,
|
|
725
|
+
rules,
|
|
726
|
+
configs
|
|
727
|
+
};
|
|
728
|
+
var index_default = uilintEslint;
|
|
729
|
+
export {
|
|
730
|
+
clearCache,
|
|
731
|
+
clearCacheEntry,
|
|
732
|
+
configs,
|
|
733
|
+
createRule,
|
|
734
|
+
index_default as default,
|
|
735
|
+
findStyleguidePath,
|
|
736
|
+
getCacheEntry,
|
|
737
|
+
getStyleguide,
|
|
738
|
+
hashContent,
|
|
739
|
+
hashContentSync,
|
|
740
|
+
loadCache,
|
|
741
|
+
loadStyleguide,
|
|
742
|
+
meta,
|
|
743
|
+
plugin,
|
|
744
|
+
rules,
|
|
745
|
+
saveCache,
|
|
746
|
+
setCacheEntry
|
|
747
|
+
};
|
|
748
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/utils/create-rule.ts","../src/rules/no-arbitrary-tailwind.ts","../src/rules/consistent-spacing.ts","../src/rules/no-direct-store-import.ts","../src/rules/no-mixed-component-libraries.ts","../src/rules/semantic.ts","../src/utils/cache.ts","../src/utils/styleguide-loader.ts","../src/index.ts"],"sourcesContent":["/**\n * Rule creation helper using @typescript-eslint/utils\n */\n\nimport { ESLintUtils } from \"@typescript-eslint/utils\";\n\nexport const createRule = ESLintUtils.RuleCreator(\n (name) =>\n `https://github.com/peter-suggate/uilint/blob/main/packages/uilint-eslint/docs/rules/${name}.md`\n);\n","/**\n * Rule: no-arbitrary-tailwind\n *\n * Forbids arbitrary Tailwind values like w-[123px], bg-[#fff], etc.\n */\n\nimport { createRule } from \"../utils/create-rule.js\";\n\ntype MessageIds = \"noArbitraryValue\";\ntype Options = [];\n\n// Regex to match arbitrary Tailwind values: word-[anything]\nconst ARBITRARY_VALUE_REGEX = /\\b[\\w-]+-\\[[^\\]]+\\]/g;\n\nexport default createRule<Options, MessageIds>({\n name: \"no-arbitrary-tailwind\",\n meta: {\n type: \"problem\",\n docs: {\n description: \"Forbid arbitrary Tailwind values like w-[123px]\",\n },\n messages: {\n noArbitraryValue:\n \"Avoid arbitrary Tailwind value '{{value}}'. Use the spacing/color scale instead.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n return {\n // Check className attributes in JSX\n JSXAttribute(node) {\n if (\n node.name.type === \"JSXIdentifier\" &&\n (node.name.name === \"className\" || node.name.name === \"class\")\n ) {\n const value = node.value;\n\n // Handle string literal: className=\"...\"\n if (value?.type === \"Literal\" && typeof value.value === \"string\") {\n checkClassString(context, value, value.value);\n }\n\n // Handle JSX expression: className={...}\n if (value?.type === \"JSXExpressionContainer\") {\n const expr = value.expression;\n\n // Direct string: className={\"...\"}\n if (expr.type === \"Literal\" && typeof expr.value === \"string\") {\n checkClassString(context, expr, expr.value);\n }\n\n // Template literal: className={`...`}\n if (expr.type === \"TemplateLiteral\") {\n for (const quasi of expr.quasis) {\n checkClassString(context, quasi, quasi.value.raw);\n }\n }\n }\n }\n },\n\n // Check cn(), clsx(), classnames() calls\n CallExpression(node) {\n if (node.callee.type !== \"Identifier\") return;\n const name = node.callee.name;\n\n if (name === \"cn\" || name === \"clsx\" || name === \"classnames\") {\n for (const arg of node.arguments) {\n if (arg.type === \"Literal\" && typeof arg.value === \"string\") {\n checkClassString(context, arg, arg.value);\n }\n if (arg.type === \"TemplateLiteral\") {\n for (const quasi of arg.quasis) {\n checkClassString(context, quasi, quasi.value.raw);\n }\n }\n }\n }\n },\n };\n },\n});\n\nfunction checkClassString(\n context: Parameters<\n ReturnType<typeof createRule<[], \"noArbitraryValue\">>[\"create\"]\n >[0],\n node: Parameters<typeof context.report>[0][\"node\"],\n classString: string\n) {\n const matches = classString.matchAll(ARBITRARY_VALUE_REGEX);\n\n for (const match of matches) {\n context.report({\n node,\n messageId: \"noArbitraryValue\",\n data: { value: match[0] },\n });\n }\n}\n","/**\n * Rule: consistent-spacing\n *\n * Enforces use of spacing scale values in gap, padding, margin utilities.\n */\n\nimport { createRule } from \"../utils/create-rule.js\";\n\ntype MessageIds = \"invalidSpacing\";\ntype Options = [\n {\n scale?: number[];\n }\n];\n\n// Default Tailwind spacing scale\nconst DEFAULT_SCALE = [\n 0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 16, 20, 24,\n 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 72, 80, 96,\n];\n\n// Spacing utilities that take numeric values\nconst SPACING_PREFIXES = [\n \"p-\",\n \"px-\",\n \"py-\",\n \"pt-\",\n \"pr-\",\n \"pb-\",\n \"pl-\",\n \"ps-\",\n \"pe-\",\n \"m-\",\n \"mx-\",\n \"my-\",\n \"mt-\",\n \"mr-\",\n \"mb-\",\n \"ml-\",\n \"ms-\",\n \"me-\",\n \"gap-\",\n \"gap-x-\",\n \"gap-y-\",\n \"space-x-\",\n \"space-y-\",\n \"inset-\",\n \"inset-x-\",\n \"inset-y-\",\n \"top-\",\n \"right-\",\n \"bottom-\",\n \"left-\",\n \"w-\",\n \"h-\",\n \"min-w-\",\n \"min-h-\",\n \"max-w-\",\n \"max-h-\",\n \"size-\",\n];\n\n// Build regex to match spacing utilities with numeric values\nfunction buildSpacingRegex(): RegExp {\n const prefixes = SPACING_PREFIXES.map((p) => p.replace(\"-\", \"\\\\-\")).join(\"|\");\n // Match prefix followed by a number (possibly with decimal)\n return new RegExp(`\\\\b(${prefixes})(\\\\d+\\\\.?\\\\d*)\\\\b`, \"g\");\n}\n\nconst SPACING_REGEX = buildSpacingRegex();\n\nexport default createRule<Options, MessageIds>({\n name: \"consistent-spacing\",\n meta: {\n type: \"suggestion\",\n docs: {\n description: \"Enforce spacing scale (no magic numbers in gap/padding)\",\n },\n messages: {\n invalidSpacing:\n \"Spacing value '{{value}}' is not in the allowed scale. Use one of: {{allowed}}\",\n },\n schema: [\n {\n type: \"object\",\n properties: {\n scale: {\n type: \"array\",\n items: { type: \"number\" },\n },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [{ scale: DEFAULT_SCALE }],\n create(context) {\n const options = context.options[0] || {};\n const scale = new Set((options.scale || DEFAULT_SCALE).map(String));\n const scaleList = [...scale].slice(0, 10).join(\", \") + \"...\";\n\n return {\n // Check className attributes in JSX\n JSXAttribute(node) {\n if (\n node.name.type === \"JSXIdentifier\" &&\n (node.name.name === \"className\" || node.name.name === \"class\")\n ) {\n const value = node.value;\n\n if (value?.type === \"Literal\" && typeof value.value === \"string\") {\n checkSpacing(context, node, value.value, scale, scaleList);\n }\n\n if (value?.type === \"JSXExpressionContainer\") {\n const expr = value.expression;\n if (expr.type === \"Literal\" && typeof expr.value === \"string\") {\n checkSpacing(context, node, expr.value, scale, scaleList);\n }\n if (expr.type === \"TemplateLiteral\") {\n for (const quasi of expr.quasis) {\n checkSpacing(context, node, quasi.value.raw, scale, scaleList);\n }\n }\n }\n }\n },\n\n // Check cn(), clsx(), classnames() calls\n CallExpression(node) {\n if (node.callee.type !== \"Identifier\") return;\n const name = node.callee.name;\n\n if (name === \"cn\" || name === \"clsx\" || name === \"classnames\") {\n for (const arg of node.arguments) {\n if (arg.type === \"Literal\" && typeof arg.value === \"string\") {\n checkSpacing(context, arg, arg.value, scale, scaleList);\n }\n if (arg.type === \"TemplateLiteral\") {\n for (const quasi of arg.quasis) {\n checkSpacing(context, quasi, quasi.value.raw, scale, scaleList);\n }\n }\n }\n }\n },\n };\n },\n});\n\nfunction checkSpacing(\n context: Parameters<\n ReturnType<typeof createRule<Options, \"invalidSpacing\">>[\"create\"]\n >[0],\n node: Parameters<typeof context.report>[0][\"node\"],\n classString: string,\n scale: Set<string>,\n scaleList: string\n) {\n // Reset regex state\n SPACING_REGEX.lastIndex = 0;\n\n let match;\n while ((match = SPACING_REGEX.exec(classString)) !== null) {\n const [, , value] = match;\n if (value && !scale.has(value)) {\n context.report({\n node,\n messageId: \"invalidSpacing\",\n data: { value, allowed: scaleList },\n });\n }\n }\n}\n","/**\n * Rule: no-direct-store-import\n *\n * Forbids direct Zustand store imports - prefer using hooks via context.\n */\n\nimport { createRule } from \"../utils/create-rule.js\";\n\ntype MessageIds = \"noDirectImport\";\ntype Options = [\n {\n storePattern?: string;\n }\n];\n\n// Convert glob pattern to regex\nfunction patternToRegex(pattern: string): RegExp {\n const escaped = pattern\n .replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\")\n .replace(/\\*/g, \".*\")\n .replace(/\\?/g, \".\");\n return new RegExp(`^${escaped}$`);\n}\n\nexport default createRule<Options, MessageIds>({\n name: \"no-direct-store-import\",\n meta: {\n type: \"problem\",\n docs: {\n description:\n \"Forbid direct Zustand store imports (use hooks via context)\",\n },\n messages: {\n noDirectImport:\n \"Avoid importing store '{{name}}' directly. Use the store via a context hook instead.\",\n },\n schema: [\n {\n type: \"object\",\n properties: {\n storePattern: {\n type: \"string\",\n description: \"Glob pattern for store names\",\n },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [{ storePattern: \"use*Store\" }],\n create(context) {\n const options = context.options[0] || {};\n const pattern = options.storePattern || \"use*Store\";\n const regex = patternToRegex(pattern);\n\n return {\n ImportDeclaration(node) {\n // Check if importing from a store file\n const source = node.source.value as string;\n if (!source.includes(\"store\")) return;\n\n // Check imported specifiers\n for (const specifier of node.specifiers) {\n if (specifier.type === \"ImportSpecifier\") {\n const importedName =\n specifier.imported.type === \"Identifier\"\n ? specifier.imported.name\n : specifier.imported.value;\n\n if (regex.test(importedName)) {\n context.report({\n node: specifier,\n messageId: \"noDirectImport\",\n data: { name: importedName },\n });\n }\n }\n\n if (specifier.type === \"ImportDefaultSpecifier\") {\n const localName = specifier.local.name;\n if (regex.test(localName)) {\n context.report({\n node: specifier,\n messageId: \"noDirectImport\",\n data: { name: localName },\n });\n }\n }\n }\n },\n };\n },\n});\n","/**\n * Rule: no-mixed-component-libraries\n *\n * Forbids mixing shadcn/ui and MUI components in the same file.\n */\n\nimport type { TSESTree } from \"@typescript-eslint/utils\";\nimport { createRule } from \"../utils/create-rule.js\";\n\ntype MessageIds = \"mixedLibraries\";\ntype LibraryName = \"shadcn\" | \"mui\";\ntype Options = [\n {\n libraries?: LibraryName[];\n }\n];\n\nconst LIBRARY_PATTERNS: Record<LibraryName, string[]> = {\n shadcn: [\"@/components/ui\", \"@radix-ui/\", \"components/ui/\"],\n mui: [\"@mui/material\", \"@mui/icons-material\", \"@emotion/\"],\n};\n\nexport default createRule<Options, MessageIds>({\n name: \"no-mixed-component-libraries\",\n meta: {\n type: \"problem\",\n docs: {\n description: \"Forbid mixing component libraries in the same file\",\n },\n messages: {\n mixedLibraries:\n \"Mixing {{lib1}} and {{lib2}} components. Choose one library per file.\",\n },\n schema: [\n {\n type: \"object\",\n properties: {\n libraries: {\n type: \"array\",\n items: { enum: [\"shadcn\", \"mui\"] },\n },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [{ libraries: [\"shadcn\", \"mui\"] }],\n create(context) {\n const options = context.options[0] || {};\n const libraries = (options.libraries || [\"shadcn\", \"mui\"]) as LibraryName[];\n const detected: Map<LibraryName, TSESTree.ImportDeclaration> = new Map();\n\n return {\n ImportDeclaration(node) {\n const source = node.source.value as string;\n\n for (const lib of libraries) {\n const patterns = LIBRARY_PATTERNS[lib];\n if (patterns?.some((p) => source.includes(p))) {\n if (!detected.has(lib)) {\n detected.set(lib, node);\n }\n }\n }\n },\n\n \"Program:exit\"() {\n if (detected.size > 1) {\n const libs = [...detected.keys()];\n const secondLib = libs[1]!;\n const secondNode = detected.get(secondLib)!;\n\n context.report({\n node: secondNode,\n messageId: \"mixedLibraries\",\n data: { lib1: libs[0], lib2: secondLib },\n });\n }\n },\n };\n },\n});\n","/**\n * Rule: semantic\n *\n * LLM-powered semantic UI analysis using the project's styleguide.\n * This is the only rule that reads .uilint/styleguide.md.\n */\n\nimport { readFileSync } from \"fs\";\nimport { dirname, relative } from \"path\";\nimport { createRule } from \"../utils/create-rule.js\";\nimport {\n getCacheEntry,\n hashContentSync,\n setCacheEntry,\n type CachedIssue,\n} from \"../utils/cache.js\";\nimport { getStyleguide } from \"../utils/styleguide-loader.js\";\n\ntype MessageIds = \"semanticIssue\" | \"styleguideNotFound\" | \"analysisError\";\ntype Options = [\n {\n model?: string;\n styleguidePath?: string;\n },\n];\n\n// Store for async analysis results that will be reported on next lint\nconst pendingAnalysis = new Map<string, Promise<CachedIssue[]>>();\n\nexport default createRule<Options, MessageIds>({\n name: \"semantic\",\n meta: {\n type: \"suggestion\",\n docs: {\n description: \"LLM-powered semantic UI analysis using styleguide\",\n },\n messages: {\n semanticIssue: \"{{message}}\",\n styleguideNotFound:\n \"No styleguide found. Create .uilint/styleguide.md or specify styleguidePath.\",\n analysisError: \"Semantic analysis failed: {{error}}\",\n },\n schema: [\n {\n type: \"object\",\n properties: {\n model: {\n type: \"string\",\n description: \"Ollama model to use\",\n },\n styleguidePath: {\n type: \"string\",\n description: \"Path to styleguide file\",\n },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [{ model: \"qwen3:8b\" }],\n create(context) {\n const options = context.options[0] || {};\n const filePath = context.filename;\n const fileDir = dirname(filePath);\n\n // Get styleguide\n const { path: styleguidePath, content: styleguide } = getStyleguide(\n fileDir,\n options.styleguidePath\n );\n\n // Skip if no styleguide\n if (!styleguide) {\n return {\n Program(node) {\n context.report({\n node,\n messageId: \"styleguideNotFound\",\n });\n },\n };\n }\n\n // Read and hash file contents\n let fileContent: string;\n try {\n fileContent = readFileSync(filePath, \"utf-8\");\n } catch {\n return {};\n }\n\n const fileHash = hashContentSync(fileContent);\n const styleguideHash = hashContentSync(styleguide);\n\n // Check cache\n const projectRoot = findProjectRoot(fileDir);\n const relativeFilePath = relative(projectRoot, filePath);\n const cached = getCacheEntry(\n projectRoot,\n relativeFilePath,\n fileHash,\n styleguideHash\n );\n\n if (cached) {\n // Report cached issues\n return {\n Program(node) {\n for (const issue of cached.issues) {\n context.report({\n node,\n loc: { line: issue.line, column: issue.column || 0 },\n messageId: \"semanticIssue\",\n data: { message: issue.message },\n });\n }\n },\n };\n }\n\n // Queue async analysis (will be picked up on next lint)\n if (!pendingAnalysis.has(filePath)) {\n const analysisPromise = runSemanticAnalysis(\n fileContent,\n styleguide,\n options.model || \"qwen3:8b\"\n );\n\n pendingAnalysis.set(filePath, analysisPromise);\n\n // Store result in cache when complete\n analysisPromise\n .then((issues) => {\n setCacheEntry(projectRoot, relativeFilePath, {\n fileHash,\n styleguideHash,\n issues,\n timestamp: Date.now(),\n });\n })\n .catch(() => {\n // Ignore errors - will retry on next lint\n })\n .finally(() => {\n pendingAnalysis.delete(filePath);\n });\n }\n\n // No issues to report yet - will be cached for next run\n return {};\n },\n});\n\n/**\n * Find project root by looking for package.json\n */\nfunction findProjectRoot(startDir: string): string {\n const { existsSync } = require(\"fs\");\n const { dirname, join } = require(\"path\");\n\n let dir = startDir;\n for (let i = 0; i < 20; i++) {\n if (existsSync(join(dir, \"package.json\"))) {\n return dir;\n }\n const parent = dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n return startDir;\n}\n\n/**\n * Run semantic analysis using Ollama\n */\nasync function runSemanticAnalysis(\n sourceCode: string,\n styleguide: string,\n model: string\n): Promise<CachedIssue[]> {\n try {\n // Dynamic import of uilint-core to avoid circular dependencies at load time\n const { OllamaClient, buildSourceScanPrompt } = await import(\"uilint-core\");\n\n const client = new OllamaClient({ model });\n\n // Check if Ollama is available\n const available = await client.isAvailable();\n if (!available) {\n console.warn(\"[uilint-eslint] Ollama not available, skipping semantic analysis\");\n return [];\n }\n\n const prompt = buildSourceScanPrompt(sourceCode, styleguide, {});\n const response = await client.complete(prompt, { json: true });\n\n const parsed = JSON.parse(response) as {\n issues?: Array<{ line?: number; column?: number; message?: string }>;\n };\n\n return (parsed.issues || []).map((issue) => ({\n line: issue.line || 1,\n column: issue.column,\n message: issue.message || \"Semantic issue detected\",\n ruleId: \"uilint/semantic\",\n severity: 1 as const,\n }));\n } catch (error) {\n console.error(\"[uilint-eslint] Semantic analysis error:\", error);\n return [];\n }\n}\n","/**\n * File-hash based caching for LLM semantic rule\n *\n * Uses xxhash for fast hashing of file contents.\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"fs\";\nimport { dirname, join } from \"path\";\n\n// Lazy-loaded xxhash\nlet xxhashInstance: Awaited<ReturnType<typeof import(\"xxhash-wasm\")[\"default\"]>> | null = null;\n\nasync function getXxhash() {\n if (!xxhashInstance) {\n const xxhash = await import(\"xxhash-wasm\");\n xxhashInstance = await xxhash.default();\n }\n return xxhashInstance;\n}\n\n/**\n * Synchronous hash using a simple djb2 algorithm (fallback when xxhash not available)\n */\nfunction djb2Hash(str: string): string {\n let hash = 5381;\n for (let i = 0; i < str.length; i++) {\n hash = (hash * 33) ^ str.charCodeAt(i);\n }\n return (hash >>> 0).toString(16);\n}\n\n/**\n * Hash content using xxhash (async) or djb2 (sync fallback)\n */\nexport async function hashContent(content: string): Promise<string> {\n try {\n const xxhash = await getXxhash();\n return xxhash.h64ToString(content);\n } catch {\n return djb2Hash(content);\n }\n}\n\n/**\n * Synchronous hash for when async is not possible\n */\nexport function hashContentSync(content: string): string {\n return djb2Hash(content);\n}\n\nexport interface CacheEntry {\n fileHash: string;\n styleguideHash: string;\n issues: CachedIssue[];\n timestamp: number;\n}\n\nexport interface CachedIssue {\n line: number;\n column?: number;\n message: string;\n ruleId: string;\n severity: 1 | 2; // 1 = warn, 2 = error\n}\n\nexport interface CacheStore {\n version: number;\n entries: Record<string, CacheEntry>;\n}\n\nconst CACHE_VERSION = 1;\nconst CACHE_FILE = \".uilint/.cache/eslint-semantic.json\";\n\n/**\n * Get the cache file path for a project\n */\nexport function getCachePath(projectRoot: string): string {\n return join(projectRoot, CACHE_FILE);\n}\n\n/**\n * Load the cache store\n */\nexport function loadCache(projectRoot: string): CacheStore {\n const cachePath = getCachePath(projectRoot);\n\n if (!existsSync(cachePath)) {\n return { version: CACHE_VERSION, entries: {} };\n }\n\n try {\n const content = readFileSync(cachePath, \"utf-8\");\n const cache = JSON.parse(content) as CacheStore;\n\n // Invalidate if version mismatch\n if (cache.version !== CACHE_VERSION) {\n return { version: CACHE_VERSION, entries: {} };\n }\n\n return cache;\n } catch {\n return { version: CACHE_VERSION, entries: {} };\n }\n}\n\n/**\n * Save the cache store\n */\nexport function saveCache(projectRoot: string, cache: CacheStore): void {\n const cachePath = getCachePath(projectRoot);\n\n try {\n const cacheDir = dirname(cachePath);\n if (!existsSync(cacheDir)) {\n mkdirSync(cacheDir, { recursive: true });\n }\n\n writeFileSync(cachePath, JSON.stringify(cache, null, 2), \"utf-8\");\n } catch {\n // Silently fail - caching is optional\n }\n}\n\n/**\n * Get cached entry for a file\n */\nexport function getCacheEntry(\n projectRoot: string,\n filePath: string,\n fileHash: string,\n styleguideHash: string\n): CacheEntry | null {\n const cache = loadCache(projectRoot);\n const entry = cache.entries[filePath];\n\n if (!entry) return null;\n\n // Check if hashes match\n if (entry.fileHash !== fileHash || entry.styleguideHash !== styleguideHash) {\n return null;\n }\n\n return entry;\n}\n\n/**\n * Set cached entry for a file\n */\nexport function setCacheEntry(\n projectRoot: string,\n filePath: string,\n entry: CacheEntry\n): void {\n const cache = loadCache(projectRoot);\n cache.entries[filePath] = entry;\n saveCache(projectRoot, cache);\n}\n\n/**\n * Clear cache for a specific file\n */\nexport function clearCacheEntry(projectRoot: string, filePath: string): void {\n const cache = loadCache(projectRoot);\n delete cache.entries[filePath];\n saveCache(projectRoot, cache);\n}\n\n/**\n * Clear entire cache\n */\nexport function clearCache(projectRoot: string): void {\n saveCache(projectRoot, { version: CACHE_VERSION, entries: {} });\n}\n","/**\n * Styleguide loader for the LLM semantic rule\n *\n * Only the semantic rule reads the styleguide - static rules use ESLint options.\n */\n\nimport { existsSync, readFileSync } from \"fs\";\nimport { dirname, isAbsolute, join, resolve } from \"path\";\n\nconst DEFAULT_STYLEGUIDE_PATHS = [\n \".uilint/styleguide.md\",\n \".uilint/styleguide.yaml\",\n \".uilint/styleguide.yml\",\n];\n\n/**\n * Find workspace root by walking up looking for pnpm-workspace.yaml, package.json, or .git\n */\nfunction findWorkspaceRoot(startDir: string): string {\n let dir = startDir;\n for (let i = 0; i < 20; i++) {\n if (\n existsSync(join(dir, \"pnpm-workspace.yaml\")) ||\n existsSync(join(dir, \".git\"))\n ) {\n return dir;\n }\n const parent = dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n return startDir;\n}\n\n/**\n * Find the styleguide file path\n */\nexport function findStyleguidePath(\n startDir: string,\n explicitPath?: string\n): string | null {\n // Explicit path takes precedence\n if (explicitPath) {\n const resolved = isAbsolute(explicitPath)\n ? explicitPath\n : resolve(startDir, explicitPath);\n if (existsSync(resolved)) {\n return resolved;\n }\n return null;\n }\n\n // Check from start dir up to workspace root\n const workspaceRoot = findWorkspaceRoot(startDir);\n let dir = startDir;\n\n while (true) {\n for (const relativePath of DEFAULT_STYLEGUIDE_PATHS) {\n const fullPath = join(dir, relativePath);\n if (existsSync(fullPath)) {\n return fullPath;\n }\n }\n\n // Stop at workspace root\n if (dir === workspaceRoot) break;\n\n const parent = dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n\n return null;\n}\n\n/**\n * Load styleguide content from file\n */\nexport function loadStyleguide(\n startDir: string,\n explicitPath?: string\n): string | null {\n const path = findStyleguidePath(startDir, explicitPath);\n if (!path) return null;\n\n try {\n return readFileSync(path, \"utf-8\");\n } catch {\n return null;\n }\n}\n\n/**\n * Get styleguide path and content\n */\nexport function getStyleguide(\n startDir: string,\n explicitPath?: string\n): { path: string | null; content: string | null } {\n const path = findStyleguidePath(startDir, explicitPath);\n if (!path) return { path: null, content: null };\n\n try {\n const content = readFileSync(path, \"utf-8\");\n return { path, content };\n } catch {\n return { path, content: null };\n }\n}\n","/**\n * UILint ESLint Plugin\n *\n * Provides ESLint rules for UI consistency checking:\n * - Static rules for common patterns (arbitrary values, spacing, component libraries)\n * - LLM-powered semantic rule that reads your styleguide\n */\n\nimport type { Linter } from \"eslint\";\nimport noArbitraryTailwind from \"./rules/no-arbitrary-tailwind.js\";\nimport consistentSpacing from \"./rules/consistent-spacing.js\";\nimport noDirectStoreImport from \"./rules/no-direct-store-import.js\";\nimport noMixedComponentLibraries from \"./rules/no-mixed-component-libraries.js\";\nimport semantic from \"./rules/semantic.js\";\n\n// Package version (injected at build time or fallback)\nconst version = \"0.1.0\";\n\n/**\n * All available rules\n */\nconst rules = {\n \"no-arbitrary-tailwind\": noArbitraryTailwind,\n \"consistent-spacing\": consistentSpacing,\n \"no-direct-store-import\": noDirectStoreImport,\n \"no-mixed-component-libraries\": noMixedComponentLibraries,\n semantic: semantic,\n};\n\n/**\n * Plugin metadata\n */\nconst meta = {\n name: \"uilint\",\n version,\n};\n\n/**\n * The ESLint plugin object\n */\nconst plugin = {\n meta,\n rules,\n};\n\n/**\n * Shared language options for all configs\n */\nconst jsxLanguageOptions: Linter.Config[\"languageOptions\"] = {\n parserOptions: {\n ecmaFeatures: {\n jsx: true,\n },\n },\n};\n\n/**\n * Recommended config - static rules only\n *\n * Usage:\n * ```js\n * import uilint from 'uilint-eslint';\n * export default [uilint.configs.recommended];\n * ```\n */\nconst recommendedConfig: Linter.Config = {\n name: \"uilint/recommended\",\n plugins: {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n uilint: plugin as any,\n },\n languageOptions: jsxLanguageOptions,\n rules: {\n \"uilint/no-arbitrary-tailwind\": \"error\",\n \"uilint/consistent-spacing\": \"warn\",\n \"uilint/no-direct-store-import\": \"warn\",\n \"uilint/no-mixed-component-libraries\": \"error\",\n },\n};\n\n/**\n * Strict config - static rules + LLM semantic rule\n *\n * Usage:\n * ```js\n * import uilint from 'uilint-eslint';\n * export default [uilint.configs.strict];\n * ```\n */\nconst strictConfig: Linter.Config = {\n name: \"uilint/strict\",\n plugins: {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n uilint: plugin as any,\n },\n languageOptions: jsxLanguageOptions,\n rules: {\n \"uilint/no-arbitrary-tailwind\": \"error\",\n \"uilint/consistent-spacing\": \"warn\",\n \"uilint/no-direct-store-import\": \"error\",\n \"uilint/no-mixed-component-libraries\": \"error\",\n \"uilint/semantic\": \"warn\",\n },\n};\n\n/**\n * Pre-configured configs\n */\nconst configs: Record<string, Linter.Config> = {\n recommended: recommendedConfig,\n strict: strictConfig,\n};\n\n/**\n * UILint ESLint export interface\n */\nexport interface UILintESLint {\n meta: typeof meta;\n plugin: typeof plugin;\n rules: typeof rules;\n configs: Record<string, Linter.Config>;\n}\n\n/**\n * Default export for ESLint flat config\n */\nconst uilintEslint: UILintESLint = {\n meta,\n plugin,\n rules,\n configs,\n};\n\nexport default uilintEslint;\n\n// Named exports for convenience\nexport { plugin, rules, configs, meta };\n\n// Re-export utilities for custom rule creation\nexport { createRule } from \"./utils/create-rule.js\";\nexport {\n loadStyleguide,\n findStyleguidePath,\n getStyleguide,\n} from \"./utils/styleguide-loader.js\";\nexport {\n hashContent,\n hashContentSync,\n getCacheEntry,\n setCacheEntry,\n clearCache,\n clearCacheEntry,\n loadCache,\n saveCache,\n type CacheEntry,\n type CachedIssue,\n type CacheStore,\n} from \"./utils/cache.js\";\n"],"mappings":";;;;;;;;AAIA,SAAS,mBAAmB;AAErB,IAAM,aAAa,YAAY;AAAA,EACpC,CAAC,SACC,uFAAuF,IAAI;AAC/F;;;ACGA,IAAM,wBAAwB;AAE9B,IAAO,gCAAQ,WAAgC;AAAA,EAC7C,MAAM;AAAA,EACN,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,MACJ,aAAa;AAAA,IACf;AAAA,IACA,UAAU;AAAA,MACR,kBACE;AAAA,IACJ;AAAA,IACA,QAAQ,CAAC;AAAA,EACX;AAAA,EACA,gBAAgB,CAAC;AAAA,EACjB,OAAO,SAAS;AACd,WAAO;AAAA;AAAA,MAEL,aAAa,MAAM;AACjB,YACE,KAAK,KAAK,SAAS,oBAClB,KAAK,KAAK,SAAS,eAAe,KAAK,KAAK,SAAS,UACtD;AACA,gBAAM,QAAQ,KAAK;AAGnB,cAAI,OAAO,SAAS,aAAa,OAAO,MAAM,UAAU,UAAU;AAChE,6BAAiB,SAAS,OAAO,MAAM,KAAK;AAAA,UAC9C;AAGA,cAAI,OAAO,SAAS,0BAA0B;AAC5C,kBAAM,OAAO,MAAM;AAGnB,gBAAI,KAAK,SAAS,aAAa,OAAO,KAAK,UAAU,UAAU;AAC7D,+BAAiB,SAAS,MAAM,KAAK,KAAK;AAAA,YAC5C;AAGA,gBAAI,KAAK,SAAS,mBAAmB;AACnC,yBAAW,SAAS,KAAK,QAAQ;AAC/B,iCAAiB,SAAS,OAAO,MAAM,MAAM,GAAG;AAAA,cAClD;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA;AAAA,MAGA,eAAe,MAAM;AACnB,YAAI,KAAK,OAAO,SAAS,aAAc;AACvC,cAAM,OAAO,KAAK,OAAO;AAEzB,YAAI,SAAS,QAAQ,SAAS,UAAU,SAAS,cAAc;AAC7D,qBAAW,OAAO,KAAK,WAAW;AAChC,gBAAI,IAAI,SAAS,aAAa,OAAO,IAAI,UAAU,UAAU;AAC3D,+BAAiB,SAAS,KAAK,IAAI,KAAK;AAAA,YAC1C;AACA,gBAAI,IAAI,SAAS,mBAAmB;AAClC,yBAAW,SAAS,IAAI,QAAQ;AAC9B,iCAAiB,SAAS,OAAO,MAAM,MAAM,GAAG;AAAA,cAClD;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF,CAAC;AAED,SAAS,iBACP,SAGA,MACA,aACA;AACA,QAAM,UAAU,YAAY,SAAS,qBAAqB;AAE1D,aAAW,SAAS,SAAS;AAC3B,YAAQ,OAAO;AAAA,MACb;AAAA,MACA,WAAW;AAAA,MACX,MAAM,EAAE,OAAO,MAAM,CAAC,EAAE;AAAA,IAC1B,CAAC;AAAA,EACH;AACF;;;ACpFA,IAAM,gBAAgB;AAAA,EACpB;AAAA,EAAG;AAAA,EAAK;AAAA,EAAG;AAAA,EAAK;AAAA,EAAG;AAAA,EAAK;AAAA,EAAG;AAAA,EAAK;AAAA,EAAG;AAAA,EAAG;AAAA,EAAG;AAAA,EAAG;AAAA,EAAG;AAAA,EAAG;AAAA,EAAI;AAAA,EAAI;AAAA,EAAI;AAAA,EAAI;AAAA,EAAI;AAAA,EAAI;AAAA,EAC1E;AAAA,EAAI;AAAA,EAAI;AAAA,EAAI;AAAA,EAAI;AAAA,EAAI;AAAA,EAAI;AAAA,EAAI;AAAA,EAAI;AAAA,EAAI;AAAA,EAAI;AAAA,EAAI;AAAA,EAAI;AAClD;AAGA,IAAM,mBAAmB;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAGA,SAAS,oBAA4B;AACnC,QAAM,WAAW,iBAAiB,IAAI,CAAC,MAAM,EAAE,QAAQ,KAAK,KAAK,CAAC,EAAE,KAAK,GAAG;AAE5E,SAAO,IAAI,OAAO,OAAO,QAAQ,sBAAsB,GAAG;AAC5D;AAEA,IAAM,gBAAgB,kBAAkB;AAExC,IAAO,6BAAQ,WAAgC;AAAA,EAC7C,MAAM;AAAA,EACN,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,MACJ,aAAa;AAAA,IACf;AAAA,IACA,UAAU;AAAA,MACR,gBACE;AAAA,IACJ;AAAA,IACA,QAAQ;AAAA,MACN;AAAA,QACE,MAAM;AAAA,QACN,YAAY;AAAA,UACV,OAAO;AAAA,YACL,MAAM;AAAA,YACN,OAAO,EAAE,MAAM,SAAS;AAAA,UAC1B;AAAA,QACF;AAAA,QACA,sBAAsB;AAAA,MACxB;AAAA,IACF;AAAA,EACF;AAAA,EACA,gBAAgB,CAAC,EAAE,OAAO,cAAc,CAAC;AAAA,EACzC,OAAO,SAAS;AACd,UAAM,UAAU,QAAQ,QAAQ,CAAC,KAAK,CAAC;AACvC,UAAM,QAAQ,IAAI,KAAK,QAAQ,SAAS,eAAe,IAAI,MAAM,CAAC;AAClE,UAAM,YAAY,CAAC,GAAG,KAAK,EAAE,MAAM,GAAG,EAAE,EAAE,KAAK,IAAI,IAAI;AAEvD,WAAO;AAAA;AAAA,MAEL,aAAa,MAAM;AACjB,YACE,KAAK,KAAK,SAAS,oBAClB,KAAK,KAAK,SAAS,eAAe,KAAK,KAAK,SAAS,UACtD;AACA,gBAAM,QAAQ,KAAK;AAEnB,cAAI,OAAO,SAAS,aAAa,OAAO,MAAM,UAAU,UAAU;AAChE,yBAAa,SAAS,MAAM,MAAM,OAAO,OAAO,SAAS;AAAA,UAC3D;AAEA,cAAI,OAAO,SAAS,0BAA0B;AAC5C,kBAAM,OAAO,MAAM;AACnB,gBAAI,KAAK,SAAS,aAAa,OAAO,KAAK,UAAU,UAAU;AAC7D,2BAAa,SAAS,MAAM,KAAK,OAAO,OAAO,SAAS;AAAA,YAC1D;AACA,gBAAI,KAAK,SAAS,mBAAmB;AACnC,yBAAW,SAAS,KAAK,QAAQ;AAC/B,6BAAa,SAAS,MAAM,MAAM,MAAM,KAAK,OAAO,SAAS;AAAA,cAC/D;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA;AAAA,MAGA,eAAe,MAAM;AACnB,YAAI,KAAK,OAAO,SAAS,aAAc;AACvC,cAAM,OAAO,KAAK,OAAO;AAEzB,YAAI,SAAS,QAAQ,SAAS,UAAU,SAAS,cAAc;AAC7D,qBAAW,OAAO,KAAK,WAAW;AAChC,gBAAI,IAAI,SAAS,aAAa,OAAO,IAAI,UAAU,UAAU;AAC3D,2BAAa,SAAS,KAAK,IAAI,OAAO,OAAO,SAAS;AAAA,YACxD;AACA,gBAAI,IAAI,SAAS,mBAAmB;AAClC,yBAAW,SAAS,IAAI,QAAQ;AAC9B,6BAAa,SAAS,OAAO,MAAM,MAAM,KAAK,OAAO,SAAS;AAAA,cAChE;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF,CAAC;AAED,SAAS,aACP,SAGA,MACA,aACA,OACA,WACA;AAEA,gBAAc,YAAY;AAE1B,MAAI;AACJ,UAAQ,QAAQ,cAAc,KAAK,WAAW,OAAO,MAAM;AACzD,UAAM,CAAC,EAAE,EAAE,KAAK,IAAI;AACpB,QAAI,SAAS,CAAC,MAAM,IAAI,KAAK,GAAG;AAC9B,cAAQ,OAAO;AAAA,QACb;AAAA,QACA,WAAW;AAAA,QACX,MAAM,EAAE,OAAO,SAAS,UAAU;AAAA,MACpC,CAAC;AAAA,IACH;AAAA,EACF;AACF;;;AC7JA,SAAS,eAAe,SAAyB;AAC/C,QAAM,UAAU,QACb,QAAQ,qBAAqB,MAAM,EACnC,QAAQ,OAAO,IAAI,EACnB,QAAQ,OAAO,GAAG;AACrB,SAAO,IAAI,OAAO,IAAI,OAAO,GAAG;AAClC;AAEA,IAAO,iCAAQ,WAAgC;AAAA,EAC7C,MAAM;AAAA,EACN,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,MACJ,aACE;AAAA,IACJ;AAAA,IACA,UAAU;AAAA,MACR,gBACE;AAAA,IACJ;AAAA,IACA,QAAQ;AAAA,MACN;AAAA,QACE,MAAM;AAAA,QACN,YAAY;AAAA,UACV,cAAc;AAAA,YACZ,MAAM;AAAA,YACN,aAAa;AAAA,UACf;AAAA,QACF;AAAA,QACA,sBAAsB;AAAA,MACxB;AAAA,IACF;AAAA,EACF;AAAA,EACA,gBAAgB,CAAC,EAAE,cAAc,YAAY,CAAC;AAAA,EAC9C,OAAO,SAAS;AACd,UAAM,UAAU,QAAQ,QAAQ,CAAC,KAAK,CAAC;AACvC,UAAM,UAAU,QAAQ,gBAAgB;AACxC,UAAM,QAAQ,eAAe,OAAO;AAEpC,WAAO;AAAA,MACL,kBAAkB,MAAM;AAEtB,cAAM,SAAS,KAAK,OAAO;AAC3B,YAAI,CAAC,OAAO,SAAS,OAAO,EAAG;AAG/B,mBAAW,aAAa,KAAK,YAAY;AACvC,cAAI,UAAU,SAAS,mBAAmB;AACxC,kBAAM,eACJ,UAAU,SAAS,SAAS,eACxB,UAAU,SAAS,OACnB,UAAU,SAAS;AAEzB,gBAAI,MAAM,KAAK,YAAY,GAAG;AAC5B,sBAAQ,OAAO;AAAA,gBACb,MAAM;AAAA,gBACN,WAAW;AAAA,gBACX,MAAM,EAAE,MAAM,aAAa;AAAA,cAC7B,CAAC;AAAA,YACH;AAAA,UACF;AAEA,cAAI,UAAU,SAAS,0BAA0B;AAC/C,kBAAM,YAAY,UAAU,MAAM;AAClC,gBAAI,MAAM,KAAK,SAAS,GAAG;AACzB,sBAAQ,OAAO;AAAA,gBACb,MAAM;AAAA,gBACN,WAAW;AAAA,gBACX,MAAM,EAAE,MAAM,UAAU;AAAA,cAC1B,CAAC;AAAA,YACH;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF,CAAC;;;AC3ED,IAAM,mBAAkD;AAAA,EACtD,QAAQ,CAAC,mBAAmB,cAAc,gBAAgB;AAAA,EAC1D,KAAK,CAAC,iBAAiB,uBAAuB,WAAW;AAC3D;AAEA,IAAO,uCAAQ,WAAgC;AAAA,EAC7C,MAAM;AAAA,EACN,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,MACJ,aAAa;AAAA,IACf;AAAA,IACA,UAAU;AAAA,MACR,gBACE;AAAA,IACJ;AAAA,IACA,QAAQ;AAAA,MACN;AAAA,QACE,MAAM;AAAA,QACN,YAAY;AAAA,UACV,WAAW;AAAA,YACT,MAAM;AAAA,YACN,OAAO,EAAE,MAAM,CAAC,UAAU,KAAK,EAAE;AAAA,UACnC;AAAA,QACF;AAAA,QACA,sBAAsB;AAAA,MACxB;AAAA,IACF;AAAA,EACF;AAAA,EACA,gBAAgB,CAAC,EAAE,WAAW,CAAC,UAAU,KAAK,EAAE,CAAC;AAAA,EACjD,OAAO,SAAS;AACd,UAAM,UAAU,QAAQ,QAAQ,CAAC,KAAK,CAAC;AACvC,UAAM,YAAa,QAAQ,aAAa,CAAC,UAAU,KAAK;AACxD,UAAM,WAAyD,oBAAI,IAAI;AAEvE,WAAO;AAAA,MACL,kBAAkB,MAAM;AACtB,cAAM,SAAS,KAAK,OAAO;AAE3B,mBAAW,OAAO,WAAW;AAC3B,gBAAM,WAAW,iBAAiB,GAAG;AACrC,cAAI,UAAU,KAAK,CAAC,MAAM,OAAO,SAAS,CAAC,CAAC,GAAG;AAC7C,gBAAI,CAAC,SAAS,IAAI,GAAG,GAAG;AACtB,uBAAS,IAAI,KAAK,IAAI;AAAA,YACxB;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,MAEA,iBAAiB;AACf,YAAI,SAAS,OAAO,GAAG;AACrB,gBAAM,OAAO,CAAC,GAAG,SAAS,KAAK,CAAC;AAChC,gBAAM,YAAY,KAAK,CAAC;AACxB,gBAAM,aAAa,SAAS,IAAI,SAAS;AAEzC,kBAAQ,OAAO;AAAA,YACb,MAAM;AAAA,YACN,WAAW;AAAA,YACX,MAAM,EAAE,MAAM,KAAK,CAAC,GAAG,MAAM,UAAU;AAAA,UACzC,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF,CAAC;;;AC1ED,SAAS,gBAAAA,qBAAoB;AAC7B,SAAS,WAAAC,UAAS,gBAAgB;;;ACFlC,SAAS,YAAY,WAAW,cAAc,qBAAqB;AACnE,SAAS,SAAS,YAAY;AAG9B,IAAI,iBAAsF;AAE1F,eAAe,YAAY;AACzB,MAAI,CAAC,gBAAgB;AACnB,UAAM,SAAS,MAAM,OAAO,aAAa;AACzC,qBAAiB,MAAM,OAAO,QAAQ;AAAA,EACxC;AACA,SAAO;AACT;AAKA,SAAS,SAAS,KAAqB;AACrC,MAAI,OAAO;AACX,WAAS,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK;AACnC,WAAQ,OAAO,KAAM,IAAI,WAAW,CAAC;AAAA,EACvC;AACA,UAAQ,SAAS,GAAG,SAAS,EAAE;AACjC;AAKA,eAAsB,YAAY,SAAkC;AAClE,MAAI;AACF,UAAM,SAAS,MAAM,UAAU;AAC/B,WAAO,OAAO,YAAY,OAAO;AAAA,EACnC,QAAQ;AACN,WAAO,SAAS,OAAO;AAAA,EACzB;AACF;AAKO,SAAS,gBAAgB,SAAyB;AACvD,SAAO,SAAS,OAAO;AACzB;AAsBA,IAAM,gBAAgB;AACtB,IAAM,aAAa;AAKZ,SAAS,aAAa,aAA6B;AACxD,SAAO,KAAK,aAAa,UAAU;AACrC;AAKO,SAAS,UAAU,aAAiC;AACzD,QAAM,YAAY,aAAa,WAAW;AAE1C,MAAI,CAAC,WAAW,SAAS,GAAG;AAC1B,WAAO,EAAE,SAAS,eAAe,SAAS,CAAC,EAAE;AAAA,EAC/C;AAEA,MAAI;AACF,UAAM,UAAU,aAAa,WAAW,OAAO;AAC/C,UAAM,QAAQ,KAAK,MAAM,OAAO;AAGhC,QAAI,MAAM,YAAY,eAAe;AACnC,aAAO,EAAE,SAAS,eAAe,SAAS,CAAC,EAAE;AAAA,IAC/C;AAEA,WAAO;AAAA,EACT,QAAQ;AACN,WAAO,EAAE,SAAS,eAAe,SAAS,CAAC,EAAE;AAAA,EAC/C;AACF;AAKO,SAAS,UAAU,aAAqB,OAAyB;AACtE,QAAM,YAAY,aAAa,WAAW;AAE1C,MAAI;AACF,UAAM,WAAW,QAAQ,SAAS;AAClC,QAAI,CAAC,WAAW,QAAQ,GAAG;AACzB,gBAAU,UAAU,EAAE,WAAW,KAAK,CAAC;AAAA,IACzC;AAEA,kBAAc,WAAW,KAAK,UAAU,OAAO,MAAM,CAAC,GAAG,OAAO;AAAA,EAClE,QAAQ;AAAA,EAER;AACF;AAKO,SAAS,cACd,aACA,UACA,UACA,gBACmB;AACnB,QAAM,QAAQ,UAAU,WAAW;AACnC,QAAM,QAAQ,MAAM,QAAQ,QAAQ;AAEpC,MAAI,CAAC,MAAO,QAAO;AAGnB,MAAI,MAAM,aAAa,YAAY,MAAM,mBAAmB,gBAAgB;AAC1E,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AAKO,SAAS,cACd,aACA,UACA,OACM;AACN,QAAM,QAAQ,UAAU,WAAW;AACnC,QAAM,QAAQ,QAAQ,IAAI;AAC1B,YAAU,aAAa,KAAK;AAC9B;AAKO,SAAS,gBAAgB,aAAqB,UAAwB;AAC3E,QAAM,QAAQ,UAAU,WAAW;AACnC,SAAO,MAAM,QAAQ,QAAQ;AAC7B,YAAU,aAAa,KAAK;AAC9B;AAKO,SAAS,WAAW,aAA2B;AACpD,YAAU,aAAa,EAAE,SAAS,eAAe,SAAS,CAAC,EAAE,CAAC;AAChE;;;ACtKA,SAAS,cAAAC,aAAY,gBAAAC,qBAAoB;AACzC,SAAS,WAAAC,UAAS,YAAY,QAAAC,OAAM,eAAe;AAEnD,IAAM,2BAA2B;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AACF;AAKA,SAAS,kBAAkB,UAA0B;AACnD,MAAI,MAAM;AACV,WAAS,IAAI,GAAG,IAAI,IAAI,KAAK;AAC3B,QACEH,YAAWG,MAAK,KAAK,qBAAqB,CAAC,KAC3CH,YAAWG,MAAK,KAAK,MAAM,CAAC,GAC5B;AACA,aAAO;AAAA,IACT;AACA,UAAM,SAASD,SAAQ,GAAG;AAC1B,QAAI,WAAW,IAAK;AACpB,UAAM;AAAA,EACR;AACA,SAAO;AACT;AAKO,SAAS,mBACd,UACA,cACe;AAEf,MAAI,cAAc;AAChB,UAAM,WAAW,WAAW,YAAY,IACpC,eACA,QAAQ,UAAU,YAAY;AAClC,QAAIF,YAAW,QAAQ,GAAG;AACxB,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT;AAGA,QAAM,gBAAgB,kBAAkB,QAAQ;AAChD,MAAI,MAAM;AAEV,SAAO,MAAM;AACX,eAAW,gBAAgB,0BAA0B;AACnD,YAAM,WAAWG,MAAK,KAAK,YAAY;AACvC,UAAIH,YAAW,QAAQ,GAAG;AACxB,eAAO;AAAA,MACT;AAAA,IACF;AAGA,QAAI,QAAQ,cAAe;AAE3B,UAAM,SAASE,SAAQ,GAAG;AAC1B,QAAI,WAAW,IAAK;AACpB,UAAM;AAAA,EACR;AAEA,SAAO;AACT;AAKO,SAAS,eACd,UACA,cACe;AACf,QAAM,OAAO,mBAAmB,UAAU,YAAY;AACtD,MAAI,CAAC,KAAM,QAAO;AAElB,MAAI;AACF,WAAOD,cAAa,MAAM,OAAO;AAAA,EACnC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAKO,SAAS,cACd,UACA,cACiD;AACjD,QAAM,OAAO,mBAAmB,UAAU,YAAY;AACtD,MAAI,CAAC,KAAM,QAAO,EAAE,MAAM,MAAM,SAAS,KAAK;AAE9C,MAAI;AACF,UAAM,UAAUA,cAAa,MAAM,OAAO;AAC1C,WAAO,EAAE,MAAM,QAAQ;AAAA,EACzB,QAAQ;AACN,WAAO,EAAE,MAAM,SAAS,KAAK;AAAA,EAC/B;AACF;;;AFjFA,IAAM,kBAAkB,oBAAI,IAAoC;AAEhE,IAAO,mBAAQ,WAAgC;AAAA,EAC7C,MAAM;AAAA,EACN,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,MACJ,aAAa;AAAA,IACf;AAAA,IACA,UAAU;AAAA,MACR,eAAe;AAAA,MACf,oBACE;AAAA,MACF,eAAe;AAAA,IACjB;AAAA,IACA,QAAQ;AAAA,MACN;AAAA,QACE,MAAM;AAAA,QACN,YAAY;AAAA,UACV,OAAO;AAAA,YACL,MAAM;AAAA,YACN,aAAa;AAAA,UACf;AAAA,UACA,gBAAgB;AAAA,YACd,MAAM;AAAA,YACN,aAAa;AAAA,UACf;AAAA,QACF;AAAA,QACA,sBAAsB;AAAA,MACxB;AAAA,IACF;AAAA,EACF;AAAA,EACA,gBAAgB,CAAC,EAAE,OAAO,WAAW,CAAC;AAAA,EACtC,OAAO,SAAS;AACd,UAAM,UAAU,QAAQ,QAAQ,CAAC,KAAK,CAAC;AACvC,UAAM,WAAW,QAAQ;AACzB,UAAM,UAAUG,SAAQ,QAAQ;AAGhC,UAAM,EAAE,MAAM,gBAAgB,SAAS,WAAW,IAAI;AAAA,MACpD;AAAA,MACA,QAAQ;AAAA,IACV;AAGA,QAAI,CAAC,YAAY;AACf,aAAO;AAAA,QACL,QAAQ,MAAM;AACZ,kBAAQ,OAAO;AAAA,YACb;AAAA,YACA,WAAW;AAAA,UACb,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF;AAGA,QAAI;AACJ,QAAI;AACF,oBAAcC,cAAa,UAAU,OAAO;AAAA,IAC9C,QAAQ;AACN,aAAO,CAAC;AAAA,IACV;AAEA,UAAM,WAAW,gBAAgB,WAAW;AAC5C,UAAM,iBAAiB,gBAAgB,UAAU;AAGjD,UAAM,cAAc,gBAAgB,OAAO;AAC3C,UAAM,mBAAmB,SAAS,aAAa,QAAQ;AACvD,UAAM,SAAS;AAAA,MACb;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAEA,QAAI,QAAQ;AAEV,aAAO;AAAA,QACL,QAAQ,MAAM;AACZ,qBAAW,SAAS,OAAO,QAAQ;AACjC,oBAAQ,OAAO;AAAA,cACb;AAAA,cACA,KAAK,EAAE,MAAM,MAAM,MAAM,QAAQ,MAAM,UAAU,EAAE;AAAA,cACnD,WAAW;AAAA,cACX,MAAM,EAAE,SAAS,MAAM,QAAQ;AAAA,YACjC,CAAC;AAAA,UACH;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAGA,QAAI,CAAC,gBAAgB,IAAI,QAAQ,GAAG;AAClC,YAAM,kBAAkB;AAAA,QACtB;AAAA,QACA;AAAA,QACA,QAAQ,SAAS;AAAA,MACnB;AAEA,sBAAgB,IAAI,UAAU,eAAe;AAG7C,sBACG,KAAK,CAAC,WAAW;AAChB,sBAAc,aAAa,kBAAkB;AAAA,UAC3C;AAAA,UACA;AAAA,UACA;AAAA,UACA,WAAW,KAAK,IAAI;AAAA,QACtB,CAAC;AAAA,MACH,CAAC,EACA,MAAM,MAAM;AAAA,MAEb,CAAC,EACA,QAAQ,MAAM;AACb,wBAAgB,OAAO,QAAQ;AAAA,MACjC,CAAC;AAAA,IACL;AAGA,WAAO,CAAC;AAAA,EACV;AACF,CAAC;AAKD,SAAS,gBAAgB,UAA0B;AACjD,QAAM,EAAE,YAAAC,YAAW,IAAI,UAAQ,IAAI;AACnC,QAAM,EAAE,SAAAF,UAAS,MAAAG,MAAK,IAAI,UAAQ,MAAM;AAExC,MAAI,MAAM;AACV,WAAS,IAAI,GAAG,IAAI,IAAI,KAAK;AAC3B,QAAID,YAAWC,MAAK,KAAK,cAAc,CAAC,GAAG;AACzC,aAAO;AAAA,IACT;AACA,UAAM,SAASH,SAAQ,GAAG;AAC1B,QAAI,WAAW,IAAK;AACpB,UAAM;AAAA,EACR;AACA,SAAO;AACT;AAKA,eAAe,oBACb,YACA,YACA,OACwB;AACxB,MAAI;AAEF,UAAM,EAAE,cAAc,sBAAsB,IAAI,MAAM,OAAO,aAAa;AAE1E,UAAM,SAAS,IAAI,aAAa,EAAE,MAAM,CAAC;AAGzC,UAAM,YAAY,MAAM,OAAO,YAAY;AAC3C,QAAI,CAAC,WAAW;AACd,cAAQ,KAAK,kEAAkE;AAC/E,aAAO,CAAC;AAAA,IACV;AAEA,UAAM,SAAS,sBAAsB,YAAY,YAAY,CAAC,CAAC;AAC/D,UAAM,WAAW,MAAM,OAAO,SAAS,QAAQ,EAAE,MAAM,KAAK,CAAC;AAE7D,UAAM,SAAS,KAAK,MAAM,QAAQ;AAIlC,YAAQ,OAAO,UAAU,CAAC,GAAG,IAAI,CAAC,WAAW;AAAA,MAC3C,MAAM,MAAM,QAAQ;AAAA,MACpB,QAAQ,MAAM;AAAA,MACd,SAAS,MAAM,WAAW;AAAA,MAC1B,QAAQ;AAAA,MACR,UAAU;AAAA,IACZ,EAAE;AAAA,EACJ,SAAS,OAAO;AACd,YAAQ,MAAM,4CAA4C,KAAK;AAC/D,WAAO,CAAC;AAAA,EACV;AACF;;;AGnMA,IAAM,UAAU;AAKhB,IAAM,QAAQ;AAAA,EACZ,yBAAyB;AAAA,EACzB,sBAAsB;AAAA,EACtB,0BAA0B;AAAA,EAC1B,gCAAgC;AAAA,EAChC,UAAU;AACZ;AAKA,IAAM,OAAO;AAAA,EACX,MAAM;AAAA,EACN;AACF;AAKA,IAAM,SAAS;AAAA,EACb;AAAA,EACA;AACF;AAKA,IAAM,qBAAuD;AAAA,EAC3D,eAAe;AAAA,IACb,cAAc;AAAA,MACZ,KAAK;AAAA,IACP;AAAA,EACF;AACF;AAWA,IAAM,oBAAmC;AAAA,EACvC,MAAM;AAAA,EACN,SAAS;AAAA;AAAA,IAEP,QAAQ;AAAA,EACV;AAAA,EACA,iBAAiB;AAAA,EACjB,OAAO;AAAA,IACL,gCAAgC;AAAA,IAChC,6BAA6B;AAAA,IAC7B,iCAAiC;AAAA,IACjC,uCAAuC;AAAA,EACzC;AACF;AAWA,IAAM,eAA8B;AAAA,EAClC,MAAM;AAAA,EACN,SAAS;AAAA;AAAA,IAEP,QAAQ;AAAA,EACV;AAAA,EACA,iBAAiB;AAAA,EACjB,OAAO;AAAA,IACL,gCAAgC;AAAA,IAChC,6BAA6B;AAAA,IAC7B,iCAAiC;AAAA,IACjC,uCAAuC;AAAA,IACvC,mBAAmB;AAAA,EACrB;AACF;AAKA,IAAM,UAAyC;AAAA,EAC7C,aAAa;AAAA,EACb,QAAQ;AACV;AAeA,IAAM,eAA6B;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,IAAO,gBAAQ;","names":["readFileSync","dirname","existsSync","readFileSync","dirname","join","dirname","readFileSync","existsSync","join"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "uilint-eslint",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "ESLint plugin for UILint - AI-powered UI consistency checking",
|
|
5
|
+
"author": "Peter Suggate",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/peter-suggate/uilint.git",
|
|
9
|
+
"directory": "packages/uilint-eslint"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/peter-suggate/uilint#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/peter-suggate/uilint/issues"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "./dist/index.js",
|
|
17
|
+
"module": "./dist/index.js",
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"import": "./dist/index.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"README.md"
|
|
28
|
+
],
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=20.0.0"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@typescript-eslint/utils": "^8.35.1",
|
|
34
|
+
"uilint-core": "^0.1.32",
|
|
35
|
+
"xxhash-wasm": "^1.1.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/eslint": "^9.6.1",
|
|
39
|
+
"@types/node": "^22.16.0",
|
|
40
|
+
"eslint": "^9.28.0",
|
|
41
|
+
"tsup": "^8.5.1",
|
|
42
|
+
"typescript": "^5.9.3"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"eslint": ">=8.0.0"
|
|
46
|
+
},
|
|
47
|
+
"keywords": [
|
|
48
|
+
"eslint",
|
|
49
|
+
"eslint-plugin",
|
|
50
|
+
"ui",
|
|
51
|
+
"lint",
|
|
52
|
+
"consistency",
|
|
53
|
+
"design-system",
|
|
54
|
+
"ai",
|
|
55
|
+
"llm",
|
|
56
|
+
"tailwind"
|
|
57
|
+
],
|
|
58
|
+
"license": "MIT",
|
|
59
|
+
"publishConfig": {
|
|
60
|
+
"access": "public"
|
|
61
|
+
},
|
|
62
|
+
"scripts": {
|
|
63
|
+
"build": "tsup",
|
|
64
|
+
"dev": "tsup --watch",
|
|
65
|
+
"typecheck": "tsc --noEmit",
|
|
66
|
+
"lint": "eslint src/"
|
|
67
|
+
}
|
|
68
|
+
}
|