uilint-eslint 0.2.167 → 0.2.168
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/dist/rules/consistent-dark-mode.js +1 -325
- package/dist/rules/consistent-dark-mode.js.map +1 -1
- package/dist/rules/enforce-absolute-imports.js +1 -325
- package/dist/rules/enforce-absolute-imports.js.map +1 -1
- package/dist/rules/no-any-in-props.js +1 -325
- package/dist/rules/no-any-in-props.js.map +1 -1
- package/dist/rules/no-direct-store-import.js +1 -325
- package/dist/rules/no-direct-store-import.js.map +1 -1
- package/dist/rules/no-mixed-component-libraries.js +14 -336
- package/dist/rules/no-mixed-component-libraries.js.map +1 -1
- package/dist/rules/no-prop-drilling-depth.js +1 -325
- package/dist/rules/no-prop-drilling-depth.js.map +1 -1
- package/dist/rules/no-raw-ui-elements.js +15 -341
- package/dist/rules/no-raw-ui-elements.js.map +1 -1
- package/dist/rules/no-secrets-in-code.js +1 -325
- package/dist/rules/no-secrets-in-code.js.map +1 -1
- package/dist/rules/no-unsafe-type-casts.js +1 -325
- package/dist/rules/no-unsafe-type-casts.js.map +1 -1
- package/dist/rules/prefer-store-selectors.js +1 -325
- package/dist/rules/prefer-store-selectors.js.map +1 -1
- package/dist/rules/prefer-tailwind.js +20 -344
- package/dist/rules/prefer-tailwind.js.map +1 -1
- package/dist/rules/prefer-zustand-state-management.js +1 -325
- package/dist/rules/prefer-zustand-state-management.js.map +1 -1
- package/dist/rules/require-input-validation.js +1 -325
- package/dist/rules/require-input-validation.js.map +1 -1
- package/dist/rules/zustand-use-selectors.js +1 -325
- package/dist/rules/zustand-use-selectors.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,329 +1,5 @@
|
|
|
1
|
-
// src/utils/create-rule.ts
|
|
2
|
-
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
3
|
-
|
|
4
|
-
// src/utils/rule-profiler.ts
|
|
5
|
-
import { appendFileSync, existsSync, mkdirSync, writeFileSync } from "fs";
|
|
6
|
-
import { isAbsolute, join, relative, resolve } from "path";
|
|
7
|
-
var DEFAULT_OUTLIER_LIMIT = 20;
|
|
8
|
-
var DEFAULT_MIN_OUTLIER_MS = 1;
|
|
9
|
-
var DEFAULT_PROFILE_DIR = ".uilint/profile";
|
|
10
|
-
var PROFILE_VERSION = 1;
|
|
11
|
-
var EXCLUDED_RULES = /* @__PURE__ */ new Set([
|
|
12
|
-
"semantic",
|
|
13
|
-
"semantic-vision",
|
|
14
|
-
"no-duplicates"
|
|
15
|
-
]);
|
|
16
|
-
var PROFILER_STATE_KEY = /* @__PURE__ */ Symbol.for("uilint.ruleProfiler.state");
|
|
17
|
-
function getGlobalState() {
|
|
18
|
-
const globalStore = globalThis;
|
|
19
|
-
if (!globalStore[PROFILER_STATE_KEY]) {
|
|
20
|
-
globalStore[PROFILER_STATE_KEY] = createState();
|
|
21
|
-
}
|
|
22
|
-
return globalStore[PROFILER_STATE_KEY];
|
|
23
|
-
}
|
|
24
|
-
function createState() {
|
|
25
|
-
return {
|
|
26
|
-
startedAtNs: process.hrtime.bigint(),
|
|
27
|
-
rules: /* @__PURE__ */ new Map(),
|
|
28
|
-
flushRegistered: false,
|
|
29
|
-
flushed: false,
|
|
30
|
-
now: () => process.hrtime.bigint()
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
var state = getGlobalState();
|
|
34
|
-
function parseBoolean(value) {
|
|
35
|
-
if (value === void 0) return true;
|
|
36
|
-
const normalized = value.trim().toLowerCase();
|
|
37
|
-
return normalized !== "0" && normalized !== "false";
|
|
38
|
-
}
|
|
39
|
-
function parsePositiveInteger(value, fallback) {
|
|
40
|
-
if (value === void 0) return fallback;
|
|
41
|
-
const parsed = Number.parseInt(value, 10);
|
|
42
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
43
|
-
}
|
|
44
|
-
function parseNonNegativeNumber(value, fallback) {
|
|
45
|
-
if (value === void 0) return fallback;
|
|
46
|
-
const parsed = Number.parseFloat(value);
|
|
47
|
-
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
|
|
48
|
-
}
|
|
49
|
-
function getRuleProfilerOptions() {
|
|
50
|
-
const profileDir = process.env.UILINT_PROFILE_DIR || DEFAULT_PROFILE_DIR;
|
|
51
|
-
return {
|
|
52
|
-
enabled: parseBoolean(process.env.UILINT_PROFILE),
|
|
53
|
-
profileDir: isAbsolute(profileDir) ? profileDir : resolve(process.cwd(), profileDir),
|
|
54
|
-
outlierLimit: parsePositiveInteger(
|
|
55
|
-
process.env.UILINT_PROFILE_OUTLIERS,
|
|
56
|
-
DEFAULT_OUTLIER_LIMIT
|
|
57
|
-
),
|
|
58
|
-
minOutlierMs: parseNonNegativeNumber(
|
|
59
|
-
process.env.UILINT_PROFILE_MIN_MS,
|
|
60
|
-
DEFAULT_MIN_OUTLIER_MS
|
|
61
|
-
)
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
function shouldProfileRule(ruleId) {
|
|
65
|
-
return getRuleProfilerOptions().enabled && !EXCLUDED_RULES.has(ruleId);
|
|
66
|
-
}
|
|
67
|
-
function normalizeProfileFilePath(filePath) {
|
|
68
|
-
if (!filePath || filePath === "<input>" || filePath === "<text>") {
|
|
69
|
-
return filePath || "<unknown>";
|
|
70
|
-
}
|
|
71
|
-
const absolute = isAbsolute(filePath) ? filePath : resolve(process.cwd(), filePath);
|
|
72
|
-
const rel = relative(process.cwd(), absolute);
|
|
73
|
-
return rel.startsWith("..") ? absolute : rel || ".";
|
|
74
|
-
}
|
|
75
|
-
function getRuleStats(ruleId) {
|
|
76
|
-
let stats = state.rules.get(ruleId);
|
|
77
|
-
if (!stats) {
|
|
78
|
-
stats = {
|
|
79
|
-
files: /* @__PURE__ */ new Map(),
|
|
80
|
-
listeners: /* @__PURE__ */ new Map()
|
|
81
|
-
};
|
|
82
|
-
state.rules.set(ruleId, stats);
|
|
83
|
-
}
|
|
84
|
-
return stats;
|
|
85
|
-
}
|
|
86
|
-
function getFileStats(ruleId, filePath) {
|
|
87
|
-
const ruleStats = getRuleStats(ruleId);
|
|
88
|
-
const normalizedPath = normalizeProfileFilePath(filePath);
|
|
89
|
-
let stats = ruleStats.files.get(normalizedPath);
|
|
90
|
-
if (!stats) {
|
|
91
|
-
stats = {
|
|
92
|
-
setupNs: 0n,
|
|
93
|
-
listenerNs: 0n,
|
|
94
|
-
listenerCalls: 0,
|
|
95
|
-
reports: 0
|
|
96
|
-
};
|
|
97
|
-
ruleStats.files.set(normalizedPath, stats);
|
|
98
|
-
}
|
|
99
|
-
return stats;
|
|
100
|
-
}
|
|
101
|
-
function getListenerStats(ruleId, selector) {
|
|
102
|
-
const ruleStats = getRuleStats(ruleId);
|
|
103
|
-
let stats = ruleStats.listeners.get(selector);
|
|
104
|
-
if (!stats) {
|
|
105
|
-
stats = {
|
|
106
|
-
totalNs: 0n,
|
|
107
|
-
calls: 0
|
|
108
|
-
};
|
|
109
|
-
ruleStats.listeners.set(selector, stats);
|
|
110
|
-
}
|
|
111
|
-
return stats;
|
|
112
|
-
}
|
|
113
|
-
function recordRuleSetup(ruleId, filePath, durationNs) {
|
|
114
|
-
if (durationNs < 0n) return;
|
|
115
|
-
const stats = getFileStats(ruleId, filePath);
|
|
116
|
-
stats.setupNs += durationNs;
|
|
117
|
-
}
|
|
118
|
-
function recordRuleListener(ruleId, filePath, selector, durationNs) {
|
|
119
|
-
if (durationNs < 0n) return;
|
|
120
|
-
const fileStats = getFileStats(ruleId, filePath);
|
|
121
|
-
fileStats.listenerNs += durationNs;
|
|
122
|
-
fileStats.listenerCalls++;
|
|
123
|
-
const listenerStats = getListenerStats(ruleId, selector);
|
|
124
|
-
listenerStats.totalNs += durationNs;
|
|
125
|
-
listenerStats.calls++;
|
|
126
|
-
}
|
|
127
|
-
function recordRuleReport(ruleId, filePath) {
|
|
128
|
-
const stats = getFileStats(ruleId, filePath);
|
|
129
|
-
stats.reports++;
|
|
130
|
-
}
|
|
131
|
-
function nsToMs(ns) {
|
|
132
|
-
return Number(ns) / 1e6;
|
|
133
|
-
}
|
|
134
|
-
function roundMs(value) {
|
|
135
|
-
return Math.round(value * 1e3) / 1e3;
|
|
136
|
-
}
|
|
137
|
-
function percentile(sortedValues, percentileValue) {
|
|
138
|
-
if (sortedValues.length === 0) return 0;
|
|
139
|
-
const index = Math.ceil(percentileValue / 100 * sortedValues.length) - 1;
|
|
140
|
-
return sortedValues[Math.min(Math.max(index, 0), sortedValues.length - 1)];
|
|
141
|
-
}
|
|
142
|
-
function buildRuleProfileSession() {
|
|
143
|
-
const options = getRuleProfilerOptions();
|
|
144
|
-
const rules = [];
|
|
145
|
-
const outliers = [];
|
|
146
|
-
const allFiles = /* @__PURE__ */ new Set();
|
|
147
|
-
for (const [ruleId, ruleStats] of state.rules) {
|
|
148
|
-
const fileTotals = [];
|
|
149
|
-
let setupMs = 0;
|
|
150
|
-
let listenerMs = 0;
|
|
151
|
-
let reports = 0;
|
|
152
|
-
let listenerCalls = 0;
|
|
153
|
-
let maxFileMs = 0;
|
|
154
|
-
for (const [filePath, fileStats] of ruleStats.files) {
|
|
155
|
-
allFiles.add(filePath);
|
|
156
|
-
const fileSetupMs = nsToMs(fileStats.setupNs);
|
|
157
|
-
const fileListenerMs = nsToMs(fileStats.listenerNs);
|
|
158
|
-
const fileTotalMs = fileSetupMs + fileListenerMs;
|
|
159
|
-
setupMs += fileSetupMs;
|
|
160
|
-
listenerMs += fileListenerMs;
|
|
161
|
-
reports += fileStats.reports;
|
|
162
|
-
listenerCalls += fileStats.listenerCalls;
|
|
163
|
-
fileTotals.push(fileTotalMs);
|
|
164
|
-
maxFileMs = Math.max(maxFileMs, fileTotalMs);
|
|
165
|
-
if (fileTotalMs >= options.minOutlierMs) {
|
|
166
|
-
outliers.push({
|
|
167
|
-
ruleId,
|
|
168
|
-
filePath,
|
|
169
|
-
totalMs: roundMs(fileTotalMs),
|
|
170
|
-
setupMs: roundMs(fileSetupMs),
|
|
171
|
-
listenerMs: roundMs(fileListenerMs),
|
|
172
|
-
listenerCalls: fileStats.listenerCalls,
|
|
173
|
-
reports: fileStats.reports
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
const sortedFileTotals = [...fileTotals].sort((a, b) => a - b);
|
|
178
|
-
const totalMs = setupMs + listenerMs;
|
|
179
|
-
const listeners = [...ruleStats.listeners.entries()].map(([selector, listenerStats]) => ({
|
|
180
|
-
selector,
|
|
181
|
-
totalMs: roundMs(nsToMs(listenerStats.totalNs)),
|
|
182
|
-
calls: listenerStats.calls
|
|
183
|
-
})).sort((a, b) => b.totalMs - a.totalMs);
|
|
184
|
-
rules.push({
|
|
185
|
-
ruleId,
|
|
186
|
-
files: ruleStats.files.size,
|
|
187
|
-
reports,
|
|
188
|
-
setupMs: roundMs(setupMs),
|
|
189
|
-
listenerMs: roundMs(listenerMs),
|
|
190
|
-
totalMs: roundMs(totalMs),
|
|
191
|
-
listenerCalls,
|
|
192
|
-
avgFileMs: ruleStats.files.size === 0 ? 0 : roundMs(totalMs / ruleStats.files.size),
|
|
193
|
-
p95FileMs: roundMs(percentile(sortedFileTotals, 95)),
|
|
194
|
-
p99FileMs: roundMs(percentile(sortedFileTotals, 99)),
|
|
195
|
-
maxFileMs: roundMs(maxFileMs),
|
|
196
|
-
listeners
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
rules.sort((a, b) => b.totalMs - a.totalMs);
|
|
200
|
-
outliers.sort((a, b) => b.totalMs - a.totalMs);
|
|
201
|
-
return {
|
|
202
|
-
version: PROFILE_VERSION,
|
|
203
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
204
|
-
durationMs: roundMs(nsToMs(state.now() - state.startedAtNs)),
|
|
205
|
-
cwd: process.cwd(),
|
|
206
|
-
nodeVersion: process.version,
|
|
207
|
-
fileCount: allFiles.size,
|
|
208
|
-
enabledRuleCount: rules.length,
|
|
209
|
-
rules,
|
|
210
|
-
outliers: outliers.slice(0, options.outlierLimit)
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
function writeProfileSession(session) {
|
|
214
|
-
const options = getRuleProfilerOptions();
|
|
215
|
-
if (!options.enabled || session.rules.length === 0) return;
|
|
216
|
-
if (!existsSync(options.profileDir)) {
|
|
217
|
-
mkdirSync(options.profileDir, { recursive: true });
|
|
218
|
-
}
|
|
219
|
-
const json = `${JSON.stringify(session, null, 2)}
|
|
220
|
-
`;
|
|
221
|
-
writeFileSync(join(options.profileDir, "latest.json"), json, "utf-8");
|
|
222
|
-
appendFileSync(
|
|
223
|
-
join(options.profileDir, "sessions.jsonl"),
|
|
224
|
-
`${JSON.stringify(session)}
|
|
225
|
-
`,
|
|
226
|
-
"utf-8"
|
|
227
|
-
);
|
|
228
|
-
}
|
|
229
|
-
function flushRuleProfiler() {
|
|
230
|
-
if (state.flushed) return null;
|
|
231
|
-
state.flushed = true;
|
|
232
|
-
const session = buildRuleProfileSession();
|
|
233
|
-
writeProfileSession(session);
|
|
234
|
-
return session;
|
|
235
|
-
}
|
|
236
|
-
function registerRuleProfilerFlush() {
|
|
237
|
-
if (state.flushRegistered) return;
|
|
238
|
-
state.flushRegistered = true;
|
|
239
|
-
process.once("beforeExit", () => {
|
|
240
|
-
flushRuleProfiler();
|
|
241
|
-
});
|
|
242
|
-
process.once("exit", () => {
|
|
243
|
-
flushRuleProfiler();
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// src/utils/create-rule.ts
|
|
248
|
-
var baseCreateRule = ESLintUtils.RuleCreator(
|
|
249
|
-
(name) => `https://github.com/peter-suggate/uilint/blob/main/packages/uilint-eslint/docs/rules/${name}.md`
|
|
250
|
-
);
|
|
251
|
-
function getContextFilename(context) {
|
|
252
|
-
return context.filename || context.getFilename?.() || "<unknown>";
|
|
253
|
-
}
|
|
254
|
-
function wrapRuleContext(context, ruleId, filePath) {
|
|
255
|
-
const reportTarget = context;
|
|
256
|
-
const originalReport = reportTarget.report.bind(context);
|
|
257
|
-
const profiledContext = Object.create(context);
|
|
258
|
-
Object.defineProperty(profiledContext, "report", {
|
|
259
|
-
value: (descriptor) => {
|
|
260
|
-
recordRuleReport(ruleId, filePath);
|
|
261
|
-
return originalReport(descriptor);
|
|
262
|
-
},
|
|
263
|
-
enumerable: true,
|
|
264
|
-
configurable: true
|
|
265
|
-
});
|
|
266
|
-
return profiledContext;
|
|
267
|
-
}
|
|
268
|
-
function wrapRuleListener(listener, ruleId, filePath) {
|
|
269
|
-
const wrapped = {};
|
|
270
|
-
for (const [selector, handler] of Object.entries(listener)) {
|
|
271
|
-
if (typeof handler !== "function") {
|
|
272
|
-
wrapped[selector] = handler;
|
|
273
|
-
continue;
|
|
274
|
-
}
|
|
275
|
-
const originalHandler = handler;
|
|
276
|
-
wrapped[selector] = function timedRuleListener(...args) {
|
|
277
|
-
const startedAt = process.hrtime.bigint();
|
|
278
|
-
try {
|
|
279
|
-
return originalHandler.apply(this, args);
|
|
280
|
-
} finally {
|
|
281
|
-
recordRuleListener(
|
|
282
|
-
ruleId,
|
|
283
|
-
filePath,
|
|
284
|
-
selector,
|
|
285
|
-
process.hrtime.bigint() - startedAt
|
|
286
|
-
);
|
|
287
|
-
}
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
return wrapped;
|
|
291
|
-
}
|
|
292
|
-
var createRule = ((config) => {
|
|
293
|
-
if (!shouldProfileRule(config.name)) {
|
|
294
|
-
return baseCreateRule(config);
|
|
295
|
-
}
|
|
296
|
-
registerRuleProfilerFlush();
|
|
297
|
-
return baseCreateRule({
|
|
298
|
-
...config,
|
|
299
|
-
create(context, optionsWithDefault) {
|
|
300
|
-
const filePath = getContextFilename(context);
|
|
301
|
-
const profiledContext = wrapRuleContext(context, config.name, filePath);
|
|
302
|
-
const startedAt = process.hrtime.bigint();
|
|
303
|
-
try {
|
|
304
|
-
const listener = config.create(profiledContext, optionsWithDefault);
|
|
305
|
-
recordRuleSetup(
|
|
306
|
-
config.name,
|
|
307
|
-
filePath,
|
|
308
|
-
process.hrtime.bigint() - startedAt
|
|
309
|
-
);
|
|
310
|
-
return wrapRuleListener(listener, config.name, filePath);
|
|
311
|
-
} catch (error) {
|
|
312
|
-
recordRuleSetup(
|
|
313
|
-
config.name,
|
|
314
|
-
filePath,
|
|
315
|
-
process.hrtime.bigint() - startedAt
|
|
316
|
-
);
|
|
317
|
-
throw error;
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
});
|
|
321
|
-
});
|
|
322
|
-
function defineRuleMeta(meta2) {
|
|
323
|
-
return meta2;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
1
|
// src/rules/consistent-dark-mode.ts
|
|
2
|
+
import { createRule, defineRuleMeta } from "uilint-eslint";
|
|
327
3
|
var meta = defineRuleMeta({
|
|
328
4
|
id: "consistent-dark-mode",
|
|
329
5
|
version: "1.0.0",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/create-rule.ts","../../src/utils/rule-profiler.ts","../../src/rules/consistent-dark-mode.ts"],"sourcesContent":["/**\n * Rule creation helper using @typescript-eslint/utils\n */\n\nimport { ESLintUtils } from \"@typescript-eslint/utils\";\nimport type { TSESLint } from \"@typescript-eslint/utils\";\nimport {\n recordRuleListener,\n recordRuleReport,\n recordRuleSetup,\n registerRuleProfilerFlush,\n shouldProfileRule,\n} from \"./rule-profiler.js\";\n\nconst baseCreateRule = ESLintUtils.RuleCreator(\n (name) =>\n `https://github.com/peter-suggate/uilint/blob/main/packages/uilint-eslint/docs/rules/${name}.md`\n);\n\ntype RuleContext<MessageIds extends string, Options extends readonly unknown[]> =\n Readonly<TSESLint.RuleContext<MessageIds, Options>>;\n\ntype ReportTarget = {\n report: (descriptor: unknown) => void;\n};\n\nfunction getContextFilename<\n MessageIds extends string,\n Options extends readonly unknown[],\n>(context: RuleContext<MessageIds, Options>): string {\n return context.filename || context.getFilename?.() || \"<unknown>\";\n}\n\nfunction wrapRuleContext<\n MessageIds extends string,\n Options extends readonly unknown[],\n>(\n context: RuleContext<MessageIds, Options>,\n ruleId: string,\n filePath: string\n): RuleContext<MessageIds, Options> {\n const reportTarget = context as unknown as ReportTarget;\n const originalReport = reportTarget.report.bind(context);\n const profiledContext = Object.create(context) as ReportTarget;\n Object.defineProperty(profiledContext, \"report\", {\n value: (descriptor: unknown) => {\n recordRuleReport(ruleId, filePath);\n return originalReport(descriptor);\n },\n enumerable: true,\n configurable: true,\n });\n\n return profiledContext as RuleContext<MessageIds, Options>;\n}\n\nfunction wrapRuleListener(\n listener: TSESLint.RuleListener,\n ruleId: string,\n filePath: string\n): TSESLint.RuleListener {\n const wrapped: Record<string, unknown> = {};\n\n for (const [selector, handler] of Object.entries(listener)) {\n if (typeof handler !== \"function\") {\n wrapped[selector] = handler;\n continue;\n }\n\n const originalHandler = handler as (...args: unknown[]) => unknown;\n wrapped[selector] = function timedRuleListener(\n this: unknown,\n ...args: unknown[]\n ): unknown {\n const startedAt = process.hrtime.bigint();\n try {\n return originalHandler.apply(this, args);\n } finally {\n recordRuleListener(\n ruleId,\n filePath,\n selector,\n process.hrtime.bigint() - startedAt\n );\n }\n };\n }\n\n return wrapped as TSESLint.RuleListener;\n}\n\nexport const createRule = (<\n Options extends readonly unknown[],\n MessageIds extends string,\n>(\n config: Parameters<typeof baseCreateRule<Options, MessageIds>>[0]\n) => {\n if (!shouldProfileRule(config.name)) {\n return baseCreateRule(config);\n }\n\n registerRuleProfilerFlush();\n\n return baseCreateRule({\n ...config,\n create(context, optionsWithDefault) {\n const filePath = getContextFilename(context);\n const profiledContext = wrapRuleContext(context, config.name, filePath);\n const startedAt = process.hrtime.bigint();\n\n try {\n const listener = config.create(profiledContext, optionsWithDefault);\n recordRuleSetup(\n config.name,\n filePath,\n process.hrtime.bigint() - startedAt\n );\n return wrapRuleListener(listener, config.name, filePath);\n } catch (error) {\n recordRuleSetup(\n config.name,\n filePath,\n process.hrtime.bigint() - startedAt\n );\n throw error;\n }\n },\n });\n}) as typeof baseCreateRule;\n\n/**\n * Schema for prompting user to configure a rule option in the CLI\n */\nexport interface OptionFieldSchema {\n /** Field name in the options object */\n key: string;\n /** Display label for the prompt */\n label: string;\n /** Prompt type */\n type: \"text\" | \"number\" | \"boolean\" | \"select\" | \"multiselect\";\n /** Default value */\n defaultValue: unknown;\n /** Placeholder text (for text/number inputs) */\n placeholder?: string;\n /** Options for select/multiselect */\n options?: Array<{ value: string | number; label: string }>;\n /** Description/hint for the field */\n description?: string;\n}\n\n/**\n * Schema describing how to prompt for rule options during installation\n */\nexport interface RuleOptionSchema {\n /** Fields that can be configured for this rule */\n fields: OptionFieldSchema[];\n}\n\n/**\n * External requirement that a rule needs to function\n */\nexport interface RuleRequirement {\n /** Requirement type for programmatic checks. Plugins define their own types. */\n type: string;\n /** Human-readable description */\n description: string;\n /** Optional: how to satisfy the requirement */\n setupHint?: string;\n}\n\n/**\n * Rule migration definition for updating rule options between versions\n */\nexport interface RuleMigration {\n /** Source version (semver) */\n from: string;\n /** Target version (semver) */\n to: string;\n /** Human-readable description of what changed */\n description: string;\n /** Function to migrate options from old format to new format */\n migrate: (oldOptions: unknown[]) => unknown[];\n /** Whether this migration contains breaking changes */\n breaking?: boolean;\n}\n\n/**\n * Colocated rule metadata - exported alongside each rule\n *\n * This structure keeps all rule metadata in the same file as the rule implementation,\n * making it easy to maintain and extend as new rules are added.\n */\nexport interface RuleMeta {\n /** Rule identifier (e.g., \"consistent-dark-mode\") - must match filename */\n id: string;\n\n /** Semantic version of the rule (e.g., \"1.0.0\") */\n version: string;\n\n /** Display name for CLI (e.g., \"No Arbitrary Tailwind\") */\n name: string;\n\n /** Short description for CLI selection prompts (one line) */\n description: string;\n\n /** Default severity level */\n defaultSeverity: \"error\" | \"warn\" | \"off\";\n\n /** Category for grouping in CLI */\n category: string;\n\n /** Icon for display in CLI/UI (emoji or icon name) */\n icon?: string;\n\n /** Short hint about the rule type/requirements */\n hint?: string;\n\n /** Whether rule is enabled by default during install */\n defaultEnabled?: boolean;\n\n /** External requirements the rule needs */\n requirements?: RuleRequirement[];\n\n /**\n * NPM packages that must be installed for this rule to work.\n * These will be added to the target project's dependencies during installation.\n *\n * Example: [\"xxhash-wasm\"] for rules using the xxhash library\n */\n npmDependencies?: string[];\n\n /** Instructions to show after installation */\n postInstallInstructions?: string;\n\n /** Framework compatibility */\n frameworks?: (\"next\" | \"vite\" | \"cra\" | \"remix\")[];\n\n /** Whether this rule requires a styleguide file */\n requiresStyleguide?: boolean;\n\n /** Default options for the rule (passed as second element in ESLint config) */\n defaultOptions?: unknown[];\n\n /** Schema for prompting user to configure options during install */\n optionSchema?: RuleOptionSchema;\n\n /**\n * Detailed documentation in markdown format.\n * Should include:\n * - What the rule does\n * - Why it's useful\n * - Examples of incorrect and correct code\n * - Configuration options explained\n */\n docs: string;\n\n /**\n * Internal utility dependencies that this rule requires.\n * When the rule is copied to a target project, these utilities\n * will be transformed to import from \"uilint-eslint\" instead\n * of relative paths.\n *\n * Example: [\"coverage-aggregator\", \"dependency-graph\"]\n */\n internalDependencies?: string[];\n\n /**\n * Whether this rule is directory-based (has lib/ folder with utilities).\n * Directory-based rules are installed as folders with index.ts and lib/ subdirectory.\n * Single-file rules are installed as single .ts files.\n *\n * When true, ESLint config imports will use:\n * ./.uilint/rules/rule-id/index.js\n * When false (default):\n * ./.uilint/rules/rule-id.js\n */\n isDirectoryBased?: boolean;\n\n /**\n * Migrations for updating rule options between versions.\n * Migrations are applied in order to transform options from older versions.\n */\n migrations?: RuleMigration[];\n\n /**\n * Which UI plugin should handle this rule.\n * Plugins define their own identifiers.\n */\n plugin?: string;\n\n /**\n * ESLint import specifier for external plugin rules.\n *\n * When set, `uilint init` will generate an import from this specifier\n * instead of looking for the rule in `.uilint/rules/`. The import should\n * be a default export of the ESLint rule implementation.\n *\n * Example: `\"uilint-vision/eslint-rules/semantic-vision\"`\n *\n * The generated ESLint config will include:\n * ```js\n * import SemanticVisionRule from \"uilint-vision/eslint-rules/semantic-vision\";\n * ```\n */\n eslintImport?: string;\n\n /**\n * Custom inspector panel ID to use for this rule's issues.\n * If not specified, uses the plugin's default issue inspector.\n * Plugins define their own panel IDs.\n */\n customInspector?: string;\n\n /**\n * Custom heatmap color for this rule's issues.\n * CSS color value (hex, rgb, hsl, or named color).\n * If not specified, uses severity-based coloring.\n */\n heatmapColor?: string;\n\n /**\n * ESLint messageIds that represent internal/sentinel errors.\n *\n * Issues reported with these messageIds are not user-facing lint issues\n * but internal error signals (e.g., \"analysis backend failed\" or\n * \"styleguide not found\"). The serve command will log these to the\n * dashboard and filter them from client results automatically.\n *\n * This allows rules with fallible backends (LLM calls, external services)\n * to signal errors through ESLint's reporting mechanism without those\n * errors being shown to end users as lint issues.\n *\n * Example: `[\"analysisError\", \"styleguideNotFound\"]`\n */\n sentinelMessageIds?: string[];\n}\n\n/**\n * Helper to define rule metadata with type safety\n */\nexport function defineRuleMeta(meta: RuleMeta): RuleMeta {\n return meta;\n}\n","/**\n * Lightweight process-local profiling for UILint ESLint rules.\n *\n * The profiler is intentionally quiet during linting: visitor callbacks only\n * update in-memory aggregates, and summaries are flushed once at process exit.\n */\n\nimport { appendFileSync, existsSync, mkdirSync, writeFileSync } from \"fs\";\nimport { isAbsolute, join, relative, resolve } from \"path\";\n\nconst DEFAULT_OUTLIER_LIMIT = 20;\nconst DEFAULT_MIN_OUTLIER_MS = 1;\nconst DEFAULT_PROFILE_DIR = \".uilint/profile\";\nconst PROFILE_VERSION = 1;\n\nconst EXCLUDED_RULES = new Set([\n \"semantic\",\n \"semantic-vision\",\n \"no-duplicates\",\n]);\n\nexport interface RuleProfilerOptions {\n enabled: boolean;\n profileDir: string;\n outlierLimit: number;\n minOutlierMs: number;\n}\n\nexport interface RuleProfileListenerSummary {\n selector: string;\n totalMs: number;\n calls: number;\n}\n\nexport interface RuleProfileSummary {\n ruleId: string;\n files: number;\n reports: number;\n setupMs: number;\n listenerMs: number;\n totalMs: number;\n listenerCalls: number;\n avgFileMs: number;\n p95FileMs: number;\n p99FileMs: number;\n maxFileMs: number;\n listeners: RuleProfileListenerSummary[];\n}\n\nexport interface RuleProfileOutlier {\n ruleId: string;\n filePath: string;\n totalMs: number;\n setupMs: number;\n listenerMs: number;\n listenerCalls: number;\n reports: number;\n}\n\nexport interface RuleProfileSession {\n version: number;\n generatedAt: string;\n durationMs: number;\n cwd: string;\n nodeVersion: string;\n fileCount: number;\n enabledRuleCount: number;\n rules: RuleProfileSummary[];\n outliers: RuleProfileOutlier[];\n}\n\ninterface ListenerStats {\n totalNs: bigint;\n calls: number;\n}\n\ninterface FileStats {\n setupNs: bigint;\n listenerNs: bigint;\n listenerCalls: number;\n reports: number;\n}\n\ninterface RuleStats {\n files: Map<string, FileStats>;\n listeners: Map<string, ListenerStats>;\n}\n\ninterface ProfilerState {\n startedAtNs: bigint;\n rules: Map<string, RuleStats>;\n flushRegistered: boolean;\n flushed: boolean;\n now: () => bigint;\n}\n\nconst PROFILER_STATE_KEY = Symbol.for(\"uilint.ruleProfiler.state\");\n\ntype GlobalWithProfiler = typeof globalThis & {\n [PROFILER_STATE_KEY]?: ProfilerState;\n};\n\nfunction getGlobalState(): ProfilerState {\n const globalStore = globalThis as GlobalWithProfiler;\n if (!globalStore[PROFILER_STATE_KEY]) {\n globalStore[PROFILER_STATE_KEY] = createState();\n }\n return globalStore[PROFILER_STATE_KEY];\n}\n\nfunction createState(): ProfilerState {\n return {\n startedAtNs: process.hrtime.bigint(),\n rules: new Map(),\n flushRegistered: false,\n flushed: false,\n now: () => process.hrtime.bigint(),\n };\n}\n\nlet state: ProfilerState = getGlobalState();\n\nfunction parseBoolean(value: string | undefined): boolean {\n if (value === undefined) return true;\n const normalized = value.trim().toLowerCase();\n return normalized !== \"0\" && normalized !== \"false\";\n}\n\nfunction parsePositiveInteger(\n value: string | undefined,\n fallback: number\n): number {\n if (value === undefined) return fallback;\n const parsed = Number.parseInt(value, 10);\n return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;\n}\n\nfunction parseNonNegativeNumber(\n value: string | undefined,\n fallback: number\n): number {\n if (value === undefined) return fallback;\n const parsed = Number.parseFloat(value);\n return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;\n}\n\nexport function getRuleProfilerOptions(): RuleProfilerOptions {\n const profileDir = process.env.UILINT_PROFILE_DIR || DEFAULT_PROFILE_DIR;\n return {\n enabled: parseBoolean(process.env.UILINT_PROFILE),\n profileDir: isAbsolute(profileDir) ? profileDir : resolve(process.cwd(), profileDir),\n outlierLimit: parsePositiveInteger(\n process.env.UILINT_PROFILE_OUTLIERS,\n DEFAULT_OUTLIER_LIMIT\n ),\n minOutlierMs: parseNonNegativeNumber(\n process.env.UILINT_PROFILE_MIN_MS,\n DEFAULT_MIN_OUTLIER_MS\n ),\n };\n}\n\nexport function shouldProfileRule(ruleId: string): boolean {\n return getRuleProfilerOptions().enabled && !EXCLUDED_RULES.has(ruleId);\n}\n\nexport function normalizeProfileFilePath(filePath: string): string {\n if (!filePath || filePath === \"<input>\" || filePath === \"<text>\") {\n return filePath || \"<unknown>\";\n }\n\n const absolute = isAbsolute(filePath) ? filePath : resolve(process.cwd(), filePath);\n const rel = relative(process.cwd(), absolute);\n return rel.startsWith(\"..\") ? absolute : rel || \".\";\n}\n\nfunction getRuleStats(ruleId: string): RuleStats {\n let stats = state.rules.get(ruleId);\n if (!stats) {\n stats = {\n files: new Map(),\n listeners: new Map(),\n };\n state.rules.set(ruleId, stats);\n }\n return stats;\n}\n\nfunction getFileStats(ruleId: string, filePath: string): FileStats {\n const ruleStats = getRuleStats(ruleId);\n const normalizedPath = normalizeProfileFilePath(filePath);\n let stats = ruleStats.files.get(normalizedPath);\n if (!stats) {\n stats = {\n setupNs: 0n,\n listenerNs: 0n,\n listenerCalls: 0,\n reports: 0,\n };\n ruleStats.files.set(normalizedPath, stats);\n }\n return stats;\n}\n\nfunction getListenerStats(ruleId: string, selector: string): ListenerStats {\n const ruleStats = getRuleStats(ruleId);\n let stats = ruleStats.listeners.get(selector);\n if (!stats) {\n stats = {\n totalNs: 0n,\n calls: 0,\n };\n ruleStats.listeners.set(selector, stats);\n }\n return stats;\n}\n\nexport function recordRuleSetup(\n ruleId: string,\n filePath: string,\n durationNs: bigint\n): void {\n if (durationNs < 0n) return;\n const stats = getFileStats(ruleId, filePath);\n stats.setupNs += durationNs;\n}\n\nexport function recordRuleListener(\n ruleId: string,\n filePath: string,\n selector: string,\n durationNs: bigint\n): void {\n if (durationNs < 0n) return;\n const fileStats = getFileStats(ruleId, filePath);\n fileStats.listenerNs += durationNs;\n fileStats.listenerCalls++;\n\n const listenerStats = getListenerStats(ruleId, selector);\n listenerStats.totalNs += durationNs;\n listenerStats.calls++;\n}\n\nexport function recordRuleReport(ruleId: string, filePath: string): void {\n const stats = getFileStats(ruleId, filePath);\n stats.reports++;\n}\n\nfunction nsToMs(ns: bigint): number {\n return Number(ns) / 1_000_000;\n}\n\nfunction roundMs(value: number): number {\n return Math.round(value * 1000) / 1000;\n}\n\nfunction percentile(sortedValues: number[], percentileValue: number): number {\n if (sortedValues.length === 0) return 0;\n const index = Math.ceil((percentileValue / 100) * sortedValues.length) - 1;\n return sortedValues[Math.min(Math.max(index, 0), sortedValues.length - 1)];\n}\n\nexport function buildRuleProfileSession(): RuleProfileSession {\n const options = getRuleProfilerOptions();\n const rules: RuleProfileSummary[] = [];\n const outliers: RuleProfileOutlier[] = [];\n const allFiles = new Set<string>();\n\n for (const [ruleId, ruleStats] of state.rules) {\n const fileTotals: number[] = [];\n let setupMs = 0;\n let listenerMs = 0;\n let reports = 0;\n let listenerCalls = 0;\n let maxFileMs = 0;\n\n for (const [filePath, fileStats] of ruleStats.files) {\n allFiles.add(filePath);\n\n const fileSetupMs = nsToMs(fileStats.setupNs);\n const fileListenerMs = nsToMs(fileStats.listenerNs);\n const fileTotalMs = fileSetupMs + fileListenerMs;\n\n setupMs += fileSetupMs;\n listenerMs += fileListenerMs;\n reports += fileStats.reports;\n listenerCalls += fileStats.listenerCalls;\n fileTotals.push(fileTotalMs);\n maxFileMs = Math.max(maxFileMs, fileTotalMs);\n\n if (fileTotalMs >= options.minOutlierMs) {\n outliers.push({\n ruleId,\n filePath,\n totalMs: roundMs(fileTotalMs),\n setupMs: roundMs(fileSetupMs),\n listenerMs: roundMs(fileListenerMs),\n listenerCalls: fileStats.listenerCalls,\n reports: fileStats.reports,\n });\n }\n }\n\n const sortedFileTotals = [...fileTotals].sort((a, b) => a - b);\n const totalMs = setupMs + listenerMs;\n const listeners = [...ruleStats.listeners.entries()]\n .map(([selector, listenerStats]) => ({\n selector,\n totalMs: roundMs(nsToMs(listenerStats.totalNs)),\n calls: listenerStats.calls,\n }))\n .sort((a, b) => b.totalMs - a.totalMs);\n\n rules.push({\n ruleId,\n files: ruleStats.files.size,\n reports,\n setupMs: roundMs(setupMs),\n listenerMs: roundMs(listenerMs),\n totalMs: roundMs(totalMs),\n listenerCalls,\n avgFileMs:\n ruleStats.files.size === 0\n ? 0\n : roundMs(totalMs / ruleStats.files.size),\n p95FileMs: roundMs(percentile(sortedFileTotals, 95)),\n p99FileMs: roundMs(percentile(sortedFileTotals, 99)),\n maxFileMs: roundMs(maxFileMs),\n listeners,\n });\n }\n\n rules.sort((a, b) => b.totalMs - a.totalMs);\n outliers.sort((a, b) => b.totalMs - a.totalMs);\n\n return {\n version: PROFILE_VERSION,\n generatedAt: new Date().toISOString(),\n durationMs: roundMs(nsToMs(state.now() - state.startedAtNs)),\n cwd: process.cwd(),\n nodeVersion: process.version,\n fileCount: allFiles.size,\n enabledRuleCount: rules.length,\n rules,\n outliers: outliers.slice(0, options.outlierLimit),\n };\n}\n\nfunction writeProfileSession(session: RuleProfileSession): void {\n const options = getRuleProfilerOptions();\n if (!options.enabled || session.rules.length === 0) return;\n\n if (!existsSync(options.profileDir)) {\n mkdirSync(options.profileDir, { recursive: true });\n }\n\n const json = `${JSON.stringify(session, null, 2)}\\n`;\n writeFileSync(join(options.profileDir, \"latest.json\"), json, \"utf-8\");\n appendFileSync(\n join(options.profileDir, \"sessions.jsonl\"),\n `${JSON.stringify(session)}\\n`,\n \"utf-8\"\n );\n}\n\nexport function flushRuleProfiler(): RuleProfileSession | null {\n if (state.flushed) return null;\n state.flushed = true;\n\n const session = buildRuleProfileSession();\n writeProfileSession(session);\n return session;\n}\n\nexport function registerRuleProfilerFlush(): void {\n if (state.flushRegistered) return;\n state.flushRegistered = true;\n\n process.once(\"beforeExit\", () => {\n flushRuleProfiler();\n });\n process.once(\"exit\", () => {\n flushRuleProfiler();\n });\n}\n\nexport function resetRuleProfilerForTests(now?: () => bigint): void {\n state = createState();\n (globalThis as GlobalWithProfiler)[PROFILER_STATE_KEY] = state;\n if (now) {\n state.now = now;\n state.startedAtNs = now();\n }\n}\n\nexport function setRuleProfilerNowForTests(now: () => bigint): void {\n state.now = now;\n}\n","/**\n * Rule: consistent-dark-mode\n *\n * Ensures consistent dark mode theming in Tailwind CSS classes.\n * - Error: When some color classes have dark: variants but others don't within the same element\n * - Warning: When Tailwind color classes are used in a file but no dark: theming exists\n */\n\nimport { createRule, defineRuleMeta } from \"../utils/create-rule.js\";\nimport type { TSESTree } from \"@typescript-eslint/utils\";\n\ntype MessageIds = \"inconsistentDarkMode\" | \"missingDarkMode\";\ntype Options = [\n {\n /** Whether to warn when no dark mode classes are found in a file that uses Tailwind colors. Default: true */\n warnOnMissingDarkMode?: boolean;\n }?\n];\n\n/**\n * Rule metadata - colocated with implementation for maintainability\n */\nexport const meta = defineRuleMeta({\n id: \"consistent-dark-mode\",\n version: \"1.0.0\",\n name: \"Consistent Dark Mode\",\n description: \"Ensure consistent dark: theming (error on mix, warn on missing)\",\n defaultSeverity: \"error\",\n category: \"static\",\n icon: \"🌓\",\n hint: \"Ensures dark mode consistency\",\n defaultEnabled: true,\n defaultOptions: [{ warnOnMissingDarkMode: true }],\n optionSchema: {\n fields: [\n {\n key: \"warnOnMissingDarkMode\",\n label: \"Warn when elements lack dark: variant\",\n type: \"boolean\",\n defaultValue: true,\n description: \"Enable warnings for elements missing dark mode variants\",\n },\n ],\n },\n docs: `\n## What it does\n\nDetects inconsistent dark mode theming in Tailwind CSS classes. Reports errors when\nsome color classes in an element have \\`dark:\\` variants but others don't, and optionally\nwarns when a file uses color classes without any dark mode theming.\n\n## Why it's useful\n\n- **Prevents broken dark mode**: Catches cases where some colors change in dark mode but others don't\n- **Encourages completeness**: Prompts you to add dark mode support where it's missing\n- **No false positives**: Only flags explicit Tailwind colors, not custom/CSS variable colors\n\n## Examples\n\n### ❌ Incorrect\n\n\\`\\`\\`tsx\n// Some colors have dark variants, others don't\n<div className=\"bg-white dark:bg-slate-900 text-black\">\n// ^^^^^^^^^ missing dark: variant\n\n// Mix of themed and unthemed\n<button className=\"bg-blue-500 dark:bg-blue-600 border-gray-300\">\n// ^^^^^^^^^^^^^^^ missing dark: variant\n\\`\\`\\`\n\n### ✅ Correct\n\n\\`\\`\\`tsx\n// All color classes have dark variants\n<div className=\"bg-white dark:bg-slate-900 text-black dark:text-white\">\n\n// Using semantic/custom colors (automatically themed via CSS variables)\n<div className=\"bg-background text-foreground\">\n<div className=\"bg-brand text-brand-foreground\">\n<div className=\"bg-primary text-primary-foreground\">\n\n// Consistent theming\n<button className=\"bg-blue-500 dark:bg-blue-600 border-gray-300 dark:border-gray-600\">\n\\`\\`\\`\n\n## Configuration\n\n\\`\\`\\`js\n// eslint.config.js\n\"uilint/consistent-dark-mode\": [\"error\", {\n warnOnMissingDarkMode: true // Warn if file uses colors without any dark mode\n}]\n\\`\\`\\`\n\n## Notes\n\n- Only explicit Tailwind colors (like \\`blue-500\\`, \\`white\\`, \\`slate-900\\`) require dark variants\n- Custom/semantic colors (\\`background\\`, \\`foreground\\`, \\`brand\\`, \\`primary\\`, etc.) are exempt\n- These are assumed to be CSS variables that handle dark mode automatically\n- Transparent, inherit, and current values are exempt\n- Non-color utilities (like \\`text-lg\\`, \\`border-2\\`) are correctly ignored\n`,\n});\n\n// Color-related class prefixes that should have dark mode variants\nconst COLOR_PREFIXES = [\n \"bg-\",\n \"text-\",\n \"border-\",\n \"border-t-\",\n \"border-r-\",\n \"border-b-\",\n \"border-l-\",\n \"border-x-\",\n \"border-y-\",\n \"ring-\",\n \"ring-offset-\",\n \"divide-\",\n \"outline-\",\n \"shadow-\",\n \"accent-\",\n \"caret-\",\n \"fill-\",\n \"stroke-\",\n \"decoration-\",\n \"placeholder-\",\n \"from-\",\n \"via-\",\n \"to-\",\n];\n\n// Values that don't need dark variants (colorless or inherited)\nconst EXEMPT_SUFFIXES = [\"transparent\", \"inherit\", \"current\", \"auto\", \"none\"];\n\n// Built-in Tailwind CSS color palette names\n// These are the ONLY colors that should trigger dark mode warnings.\n// Custom colors (like 'brand', 'company-primary') are assumed to be\n// CSS variables that handle dark mode automatically.\nconst TAILWIND_COLOR_NAMES = new Set([\n // Special colors\n \"white\",\n \"black\",\n // Gray scale palettes\n \"slate\",\n \"gray\",\n \"zinc\",\n \"neutral\",\n \"stone\",\n // Warm colors\n \"red\",\n \"orange\",\n \"amber\",\n \"yellow\",\n // Green colors\n \"lime\",\n \"green\",\n \"emerald\",\n \"teal\",\n // Blue colors\n \"cyan\",\n \"sky\",\n \"blue\",\n \"indigo\",\n // Purple/Pink colors\n \"violet\",\n \"purple\",\n \"fuchsia\",\n \"pink\",\n \"rose\",\n]);\n\n/**\n * Check if a class has 'dark' in its variant chain\n */\nfunction hasDarkVariant(className: string): boolean {\n const parts = className.split(\":\");\n // All parts except the last are variants\n const variants = parts.slice(0, -1);\n return variants.includes(\"dark\");\n}\n\n/**\n * Get the base class (without any variants like hover:, dark:, md:, etc.)\n */\nfunction getBaseClass(className: string): string {\n const parts = className.split(\":\");\n return parts[parts.length - 1] || \"\";\n}\n\n/**\n * Find the color prefix this class uses, if any\n */\nfunction getColorPrefix(baseClass: string): string | null {\n // Sort by length descending to match more specific prefixes first\n // (e.g., \"border-t-\" before \"border-\")\n const sortedPrefixes = [...COLOR_PREFIXES].sort(\n (a, b) => b.length - a.length\n );\n return sortedPrefixes.find((p) => baseClass.startsWith(p)) || null;\n}\n\n/**\n * Check if the value is an explicit Tailwind color.\n * Uses an allowlist approach: only built-in Tailwind color names trigger warnings.\n * Custom colors (like 'brand', 'primary', 'company-blue') are assumed to be\n * CSS variables that handle dark mode automatically and should NOT trigger.\n *\n * Matches patterns like:\n * - white, black (standalone colors)\n * - blue-500, slate-900 (color-scale)\n * - blue-500/50, gray-900/80 (with opacity modifier)\n */\nfunction isTailwindColor(value: string): boolean {\n // Remove opacity modifier if present (e.g., \"blue-500/50\" -> \"blue-500\")\n const valueWithoutOpacity = value.split(\"/\")[0] || value;\n\n // Check for standalone colors (white, black)\n if (TAILWIND_COLOR_NAMES.has(valueWithoutOpacity)) {\n return true;\n }\n\n // Check for color-scale pattern (e.g., \"blue-500\", \"slate-900\")\n // Pattern: colorName-number where number is 50, 100, 200, ..., 950\n const match = valueWithoutOpacity.match(/^([a-z]+)-(\\d+)$/);\n if (match) {\n const colorName = match[1];\n const scale = match[2];\n // Valid Tailwind scales are: 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950\n const validScales = [\n \"50\",\n \"100\",\n \"200\",\n \"300\",\n \"400\",\n \"500\",\n \"600\",\n \"700\",\n \"800\",\n \"900\",\n \"950\",\n ];\n if (colorName && TAILWIND_COLOR_NAMES.has(colorName) && validScales.includes(scale || \"\")) {\n return true;\n }\n }\n\n return false;\n}\n\n/**\n * Check if the value after the prefix looks like an explicit Tailwind color.\n * Uses allowlist approach: only built-in Tailwind colors should trigger dark mode warnings.\n * Custom/semantic colors (brand, primary, foreground, etc.) are NOT flagged.\n */\nfunction isColorValue(baseClass: string, prefix: string): boolean {\n const value = baseClass.slice(prefix.length);\n\n // Empty value is not a color\n if (!value) {\n return false;\n }\n\n // Only flag explicit Tailwind colors\n // Custom colors, CSS variable colors, and semantic colors are exempt\n return isTailwindColor(value);\n}\n\n/**\n * Check if a class is exempt from dark mode requirements\n */\nfunction isExempt(baseClass: string): boolean {\n return EXEMPT_SUFFIXES.some((suffix) => baseClass.endsWith(suffix));\n}\n\nexport default createRule<Options, MessageIds>({\n name: \"consistent-dark-mode\",\n meta: {\n type: \"problem\",\n docs: {\n description: \"Ensure consistent dark mode theming in Tailwind classes\",\n },\n messages: {\n inconsistentDarkMode:\n \"Inconsistent dark mode: '{{unthemed}}' lack dark: variants while other color classes have them.\",\n missingDarkMode:\n \"No dark mode theming detected. Consider adding dark: variants for color classes.\",\n },\n schema: [\n {\n type: \"object\",\n properties: {\n warnOnMissingDarkMode: {\n type: \"boolean\",\n description:\n \"Whether to warn when no dark mode classes are found in a file that uses Tailwind colors\",\n },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [{ warnOnMissingDarkMode: true }],\n create(context) {\n const options = context.options[0] || {};\n const warnOnMissingDarkMode = options.warnOnMissingDarkMode ?? true;\n\n let fileHasColorClasses = false;\n let fileHasDarkMode = false;\n const reportedNodes = new Set<TSESTree.Node>();\n\n function checkClassString(node: TSESTree.Node, classString: string) {\n const classes = classString.split(/\\s+/).filter(Boolean);\n if (classes.length === 0) return;\n\n // Track usage per color prefix: { hasLight, hasDark, lightClasses }\n const prefixUsage = new Map<\n string,\n { hasLight: boolean; hasDark: boolean; lightClasses: string[] }\n >();\n\n for (const cls of classes) {\n const baseClass = getBaseClass(cls);\n const prefix = getColorPrefix(baseClass);\n\n if (!prefix) continue;\n if (isExempt(baseClass)) continue;\n\n // Verify this is actually a color class, not something like text-lg\n if (!isColorValue(baseClass, prefix)) continue;\n\n if (!prefixUsage.has(prefix)) {\n prefixUsage.set(prefix, {\n hasLight: false,\n hasDark: false,\n lightClasses: [],\n });\n }\n\n const usage = prefixUsage.get(prefix)!;\n\n if (hasDarkVariant(cls)) {\n usage.hasDark = true;\n fileHasDarkMode = true;\n } else {\n usage.hasLight = true;\n usage.lightClasses.push(cls);\n }\n }\n\n // Track if file uses color classes\n if (prefixUsage.size > 0) {\n fileHasColorClasses = true;\n }\n\n // Check for inconsistency: some prefixes have dark variants, others don't\n const entries = Array.from(prefixUsage.entries());\n const hasSomeDark = entries.some(([_, u]) => u.hasDark);\n\n if (hasSomeDark) {\n const unthemedEntries = entries.filter(\n ([_, usage]) => usage.hasLight && !usage.hasDark\n );\n\n if (unthemedEntries.length > 0 && !reportedNodes.has(node)) {\n reportedNodes.add(node);\n // Collect the actual class names that lack dark variants\n const unthemedClasses = unthemedEntries.flatMap(\n ([_, u]) => u.lightClasses\n );\n\n context.report({\n node,\n messageId: \"inconsistentDarkMode\",\n data: { unthemed: unthemedClasses.join(\", \") },\n });\n }\n }\n }\n\n function processStringValue(node: TSESTree.Node, value: string) {\n checkClassString(node, value);\n }\n\n function processTemplateLiteral(node: TSESTree.TemplateLiteral) {\n for (const quasi of node.quasis) {\n checkClassString(quasi, quasi.value.raw);\n }\n }\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 // Handle string literal: className=\"...\"\n if (value?.type === \"Literal\" && typeof value.value === \"string\") {\n processStringValue(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 processStringValue(expr, expr.value);\n }\n\n // Template literal: className={`...`}\n if (expr.type === \"TemplateLiteral\") {\n processTemplateLiteral(expr);\n }\n }\n }\n },\n\n // Check cn(), clsx(), classnames(), cva() calls\n CallExpression(node) {\n if (node.callee.type !== \"Identifier\") return;\n const name = node.callee.name;\n\n if (\n name === \"cn\" ||\n name === \"clsx\" ||\n name === \"classnames\" ||\n name === \"cva\" ||\n name === \"twMerge\"\n ) {\n for (const arg of node.arguments) {\n if (arg.type === \"Literal\" && typeof arg.value === \"string\") {\n processStringValue(arg, arg.value);\n }\n if (arg.type === \"TemplateLiteral\") {\n processTemplateLiteral(arg);\n }\n // Handle arrays of class strings\n if (arg.type === \"ArrayExpression\") {\n for (const element of arg.elements) {\n if (\n element?.type === \"Literal\" &&\n typeof element.value === \"string\"\n ) {\n processStringValue(element, element.value);\n }\n if (element?.type === \"TemplateLiteral\") {\n processTemplateLiteral(element);\n }\n }\n }\n }\n }\n },\n\n // At the end of the file, check if Tailwind colors are used without any dark mode\n \"Program:exit\"(node) {\n if (warnOnMissingDarkMode && fileHasColorClasses && !fileHasDarkMode) {\n context.report({\n node,\n messageId: \"missingDarkMode\",\n });\n }\n },\n };\n },\n});\n"],"mappings":"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","names":["meta"]}
|
|
1
|
+
{"version":3,"sources":["../../src/rules/consistent-dark-mode.ts"],"sourcesContent":["/**\n * Rule: consistent-dark-mode\n *\n * Ensures consistent dark mode theming in Tailwind CSS classes.\n * - Error: When some color classes have dark: variants but others don't within the same element\n * - Warning: When Tailwind color classes are used in a file but no dark: theming exists\n */\n\nimport { createRule, defineRuleMeta } from \"../utils/create-rule.js\";\nimport type { TSESTree } from \"@typescript-eslint/utils\";\n\ntype MessageIds = \"inconsistentDarkMode\" | \"missingDarkMode\";\ntype Options = [\n {\n /** Whether to warn when no dark mode classes are found in a file that uses Tailwind colors. Default: true */\n warnOnMissingDarkMode?: boolean;\n }?\n];\n\n/**\n * Rule metadata - colocated with implementation for maintainability\n */\nexport const meta = defineRuleMeta({\n id: \"consistent-dark-mode\",\n version: \"1.0.0\",\n name: \"Consistent Dark Mode\",\n description: \"Ensure consistent dark: theming (error on mix, warn on missing)\",\n defaultSeverity: \"error\",\n category: \"static\",\n icon: \"🌓\",\n hint: \"Ensures dark mode consistency\",\n defaultEnabled: true,\n defaultOptions: [{ warnOnMissingDarkMode: true }],\n optionSchema: {\n fields: [\n {\n key: \"warnOnMissingDarkMode\",\n label: \"Warn when elements lack dark: variant\",\n type: \"boolean\",\n defaultValue: true,\n description: \"Enable warnings for elements missing dark mode variants\",\n },\n ],\n },\n docs: `\n## What it does\n\nDetects inconsistent dark mode theming in Tailwind CSS classes. Reports errors when\nsome color classes in an element have \\`dark:\\` variants but others don't, and optionally\nwarns when a file uses color classes without any dark mode theming.\n\n## Why it's useful\n\n- **Prevents broken dark mode**: Catches cases where some colors change in dark mode but others don't\n- **Encourages completeness**: Prompts you to add dark mode support where it's missing\n- **No false positives**: Only flags explicit Tailwind colors, not custom/CSS variable colors\n\n## Examples\n\n### ❌ Incorrect\n\n\\`\\`\\`tsx\n// Some colors have dark variants, others don't\n<div className=\"bg-white dark:bg-slate-900 text-black\">\n// ^^^^^^^^^ missing dark: variant\n\n// Mix of themed and unthemed\n<button className=\"bg-blue-500 dark:bg-blue-600 border-gray-300\">\n// ^^^^^^^^^^^^^^^ missing dark: variant\n\\`\\`\\`\n\n### ✅ Correct\n\n\\`\\`\\`tsx\n// All color classes have dark variants\n<div className=\"bg-white dark:bg-slate-900 text-black dark:text-white\">\n\n// Using semantic/custom colors (automatically themed via CSS variables)\n<div className=\"bg-background text-foreground\">\n<div className=\"bg-brand text-brand-foreground\">\n<div className=\"bg-primary text-primary-foreground\">\n\n// Consistent theming\n<button className=\"bg-blue-500 dark:bg-blue-600 border-gray-300 dark:border-gray-600\">\n\\`\\`\\`\n\n## Configuration\n\n\\`\\`\\`js\n// eslint.config.js\n\"uilint/consistent-dark-mode\": [\"error\", {\n warnOnMissingDarkMode: true // Warn if file uses colors without any dark mode\n}]\n\\`\\`\\`\n\n## Notes\n\n- Only explicit Tailwind colors (like \\`blue-500\\`, \\`white\\`, \\`slate-900\\`) require dark variants\n- Custom/semantic colors (\\`background\\`, \\`foreground\\`, \\`brand\\`, \\`primary\\`, etc.) are exempt\n- These are assumed to be CSS variables that handle dark mode automatically\n- Transparent, inherit, and current values are exempt\n- Non-color utilities (like \\`text-lg\\`, \\`border-2\\`) are correctly ignored\n`,\n});\n\n// Color-related class prefixes that should have dark mode variants\nconst COLOR_PREFIXES = [\n \"bg-\",\n \"text-\",\n \"border-\",\n \"border-t-\",\n \"border-r-\",\n \"border-b-\",\n \"border-l-\",\n \"border-x-\",\n \"border-y-\",\n \"ring-\",\n \"ring-offset-\",\n \"divide-\",\n \"outline-\",\n \"shadow-\",\n \"accent-\",\n \"caret-\",\n \"fill-\",\n \"stroke-\",\n \"decoration-\",\n \"placeholder-\",\n \"from-\",\n \"via-\",\n \"to-\",\n];\n\n// Values that don't need dark variants (colorless or inherited)\nconst EXEMPT_SUFFIXES = [\"transparent\", \"inherit\", \"current\", \"auto\", \"none\"];\n\n// Built-in Tailwind CSS color palette names\n// These are the ONLY colors that should trigger dark mode warnings.\n// Custom colors (like 'brand', 'company-primary') are assumed to be\n// CSS variables that handle dark mode automatically.\nconst TAILWIND_COLOR_NAMES = new Set([\n // Special colors\n \"white\",\n \"black\",\n // Gray scale palettes\n \"slate\",\n \"gray\",\n \"zinc\",\n \"neutral\",\n \"stone\",\n // Warm colors\n \"red\",\n \"orange\",\n \"amber\",\n \"yellow\",\n // Green colors\n \"lime\",\n \"green\",\n \"emerald\",\n \"teal\",\n // Blue colors\n \"cyan\",\n \"sky\",\n \"blue\",\n \"indigo\",\n // Purple/Pink colors\n \"violet\",\n \"purple\",\n \"fuchsia\",\n \"pink\",\n \"rose\",\n]);\n\n/**\n * Check if a class has 'dark' in its variant chain\n */\nfunction hasDarkVariant(className: string): boolean {\n const parts = className.split(\":\");\n // All parts except the last are variants\n const variants = parts.slice(0, -1);\n return variants.includes(\"dark\");\n}\n\n/**\n * Get the base class (without any variants like hover:, dark:, md:, etc.)\n */\nfunction getBaseClass(className: string): string {\n const parts = className.split(\":\");\n return parts[parts.length - 1] || \"\";\n}\n\n/**\n * Find the color prefix this class uses, if any\n */\nfunction getColorPrefix(baseClass: string): string | null {\n // Sort by length descending to match more specific prefixes first\n // (e.g., \"border-t-\" before \"border-\")\n const sortedPrefixes = [...COLOR_PREFIXES].sort(\n (a, b) => b.length - a.length\n );\n return sortedPrefixes.find((p) => baseClass.startsWith(p)) || null;\n}\n\n/**\n * Check if the value is an explicit Tailwind color.\n * Uses an allowlist approach: only built-in Tailwind color names trigger warnings.\n * Custom colors (like 'brand', 'primary', 'company-blue') are assumed to be\n * CSS variables that handle dark mode automatically and should NOT trigger.\n *\n * Matches patterns like:\n * - white, black (standalone colors)\n * - blue-500, slate-900 (color-scale)\n * - blue-500/50, gray-900/80 (with opacity modifier)\n */\nfunction isTailwindColor(value: string): boolean {\n // Remove opacity modifier if present (e.g., \"blue-500/50\" -> \"blue-500\")\n const valueWithoutOpacity = value.split(\"/\")[0] || value;\n\n // Check for standalone colors (white, black)\n if (TAILWIND_COLOR_NAMES.has(valueWithoutOpacity)) {\n return true;\n }\n\n // Check for color-scale pattern (e.g., \"blue-500\", \"slate-900\")\n // Pattern: colorName-number where number is 50, 100, 200, ..., 950\n const match = valueWithoutOpacity.match(/^([a-z]+)-(\\d+)$/);\n if (match) {\n const colorName = match[1];\n const scale = match[2];\n // Valid Tailwind scales are: 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950\n const validScales = [\n \"50\",\n \"100\",\n \"200\",\n \"300\",\n \"400\",\n \"500\",\n \"600\",\n \"700\",\n \"800\",\n \"900\",\n \"950\",\n ];\n if (colorName && TAILWIND_COLOR_NAMES.has(colorName) && validScales.includes(scale || \"\")) {\n return true;\n }\n }\n\n return false;\n}\n\n/**\n * Check if the value after the prefix looks like an explicit Tailwind color.\n * Uses allowlist approach: only built-in Tailwind colors should trigger dark mode warnings.\n * Custom/semantic colors (brand, primary, foreground, etc.) are NOT flagged.\n */\nfunction isColorValue(baseClass: string, prefix: string): boolean {\n const value = baseClass.slice(prefix.length);\n\n // Empty value is not a color\n if (!value) {\n return false;\n }\n\n // Only flag explicit Tailwind colors\n // Custom colors, CSS variable colors, and semantic colors are exempt\n return isTailwindColor(value);\n}\n\n/**\n * Check if a class is exempt from dark mode requirements\n */\nfunction isExempt(baseClass: string): boolean {\n return EXEMPT_SUFFIXES.some((suffix) => baseClass.endsWith(suffix));\n}\n\nexport default createRule<Options, MessageIds>({\n name: \"consistent-dark-mode\",\n meta: {\n type: \"problem\",\n docs: {\n description: \"Ensure consistent dark mode theming in Tailwind classes\",\n },\n messages: {\n inconsistentDarkMode:\n \"Inconsistent dark mode: '{{unthemed}}' lack dark: variants while other color classes have them.\",\n missingDarkMode:\n \"No dark mode theming detected. Consider adding dark: variants for color classes.\",\n },\n schema: [\n {\n type: \"object\",\n properties: {\n warnOnMissingDarkMode: {\n type: \"boolean\",\n description:\n \"Whether to warn when no dark mode classes are found in a file that uses Tailwind colors\",\n },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [{ warnOnMissingDarkMode: true }],\n create(context) {\n const options = context.options[0] || {};\n const warnOnMissingDarkMode = options.warnOnMissingDarkMode ?? true;\n\n let fileHasColorClasses = false;\n let fileHasDarkMode = false;\n const reportedNodes = new Set<TSESTree.Node>();\n\n function checkClassString(node: TSESTree.Node, classString: string) {\n const classes = classString.split(/\\s+/).filter(Boolean);\n if (classes.length === 0) return;\n\n // Track usage per color prefix: { hasLight, hasDark, lightClasses }\n const prefixUsage = new Map<\n string,\n { hasLight: boolean; hasDark: boolean; lightClasses: string[] }\n >();\n\n for (const cls of classes) {\n const baseClass = getBaseClass(cls);\n const prefix = getColorPrefix(baseClass);\n\n if (!prefix) continue;\n if (isExempt(baseClass)) continue;\n\n // Verify this is actually a color class, not something like text-lg\n if (!isColorValue(baseClass, prefix)) continue;\n\n if (!prefixUsage.has(prefix)) {\n prefixUsage.set(prefix, {\n hasLight: false,\n hasDark: false,\n lightClasses: [],\n });\n }\n\n const usage = prefixUsage.get(prefix)!;\n\n if (hasDarkVariant(cls)) {\n usage.hasDark = true;\n fileHasDarkMode = true;\n } else {\n usage.hasLight = true;\n usage.lightClasses.push(cls);\n }\n }\n\n // Track if file uses color classes\n if (prefixUsage.size > 0) {\n fileHasColorClasses = true;\n }\n\n // Check for inconsistency: some prefixes have dark variants, others don't\n const entries = Array.from(prefixUsage.entries());\n const hasSomeDark = entries.some(([_, u]) => u.hasDark);\n\n if (hasSomeDark) {\n const unthemedEntries = entries.filter(\n ([_, usage]) => usage.hasLight && !usage.hasDark\n );\n\n if (unthemedEntries.length > 0 && !reportedNodes.has(node)) {\n reportedNodes.add(node);\n // Collect the actual class names that lack dark variants\n const unthemedClasses = unthemedEntries.flatMap(\n ([_, u]) => u.lightClasses\n );\n\n context.report({\n node,\n messageId: \"inconsistentDarkMode\",\n data: { unthemed: unthemedClasses.join(\", \") },\n });\n }\n }\n }\n\n function processStringValue(node: TSESTree.Node, value: string) {\n checkClassString(node, value);\n }\n\n function processTemplateLiteral(node: TSESTree.TemplateLiteral) {\n for (const quasi of node.quasis) {\n checkClassString(quasi, quasi.value.raw);\n }\n }\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 // Handle string literal: className=\"...\"\n if (value?.type === \"Literal\" && typeof value.value === \"string\") {\n processStringValue(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 processStringValue(expr, expr.value);\n }\n\n // Template literal: className={`...`}\n if (expr.type === \"TemplateLiteral\") {\n processTemplateLiteral(expr);\n }\n }\n }\n },\n\n // Check cn(), clsx(), classnames(), cva() calls\n CallExpression(node) {\n if (node.callee.type !== \"Identifier\") return;\n const name = node.callee.name;\n\n if (\n name === \"cn\" ||\n name === \"clsx\" ||\n name === \"classnames\" ||\n name === \"cva\" ||\n name === \"twMerge\"\n ) {\n for (const arg of node.arguments) {\n if (arg.type === \"Literal\" && typeof arg.value === \"string\") {\n processStringValue(arg, arg.value);\n }\n if (arg.type === \"TemplateLiteral\") {\n processTemplateLiteral(arg);\n }\n // Handle arrays of class strings\n if (arg.type === \"ArrayExpression\") {\n for (const element of arg.elements) {\n if (\n element?.type === \"Literal\" &&\n typeof element.value === \"string\"\n ) {\n processStringValue(element, element.value);\n }\n if (element?.type === \"TemplateLiteral\") {\n processTemplateLiteral(element);\n }\n }\n }\n }\n }\n },\n\n // At the end of the file, check if Tailwind colors are used without any dark mode\n \"Program:exit\"(node) {\n if (warnOnMissingDarkMode && fileHasColorClasses && !fileHasDarkMode) {\n context.report({\n node,\n messageId: \"missingDarkMode\",\n });\n }\n },\n };\n },\n});\n"],"mappings":"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","names":[]}
|