@tailwind-ts/eslint-plugin 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -0
- package/index.js +9 -0
- package/kelly.js +56 -0
- package/package.json +20 -80
- package/scripts/install-check.cjs +220 -0
- package/LICENSE +0 -21
- package/docs/rules/no-invalid-apply.md +0 -37
- package/docs/rules/no-invalid-classname.md +0 -39
- package/lib/index.cjs +0 -370
- package/lib/index.d.cts +0 -33
- package/lib/index.d.mts +0 -33
- package/lib/index.d.ts +0 -33
- package/lib/index.mjs +0 -367
- package/lib/worker/validate.mjs +0 -65
package/lib/index.mjs
DELETED
|
@@ -1,367 +0,0 @@
|
|
|
1
|
-
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
2
|
-
import { existsSync, statSync } from 'node:fs';
|
|
3
|
-
import { dirname, join, isAbsolute, resolve } from 'node:path';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { createSyncFn } from 'synckit';
|
|
6
|
-
|
|
7
|
-
const name = "@tailwind-ts/eslint-plugin";
|
|
8
|
-
const version = "0.1.0";
|
|
9
|
-
const packageJson = {
|
|
10
|
-
name: name,
|
|
11
|
-
version: version};
|
|
12
|
-
|
|
13
|
-
function isStringLiteral(node) {
|
|
14
|
-
return node.type === "Literal" && typeof node.value === "string";
|
|
15
|
-
}
|
|
16
|
-
function collectFromExpression(node, ignoredKeys, functionNames, literals) {
|
|
17
|
-
if (!node || node.type === "SpreadElement") {
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
20
|
-
if (isStringLiteral(node)) {
|
|
21
|
-
literals.push({ value: node.value, node });
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
if (node.type === "TemplateLiteral" && node.expressions.length === 0) {
|
|
25
|
-
const value = node.quasis.map((quasi) => quasi.value.cooked ?? "").join("");
|
|
26
|
-
literals.push({ value, node });
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
if (node.type === "LogicalExpression") {
|
|
30
|
-
collectFromExpression(node.left, ignoredKeys, functionNames, literals);
|
|
31
|
-
collectFromExpression(node.right, ignoredKeys, functionNames, literals);
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
if (node.type === "ConditionalExpression") {
|
|
35
|
-
collectFromExpression(node.consequent, ignoredKeys, functionNames, literals);
|
|
36
|
-
collectFromExpression(node.alternate, ignoredKeys, functionNames, literals);
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
if (node.type === "ArrayExpression") {
|
|
40
|
-
for (const element of node.elements) {
|
|
41
|
-
collectFromExpression(element, ignoredKeys, functionNames, literals);
|
|
42
|
-
}
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
if (node.type === "ObjectExpression") {
|
|
46
|
-
for (const property of node.properties) {
|
|
47
|
-
if (property.type === "Property") {
|
|
48
|
-
if (property.key.type === "Identifier") {
|
|
49
|
-
if (ignoredKeys.has(property.key.name)) {
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
collectFromExpression(
|
|
54
|
-
property.value,
|
|
55
|
-
ignoredKeys,
|
|
56
|
-
functionNames,
|
|
57
|
-
literals
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
if (node.type === "CallExpression" && node.callee.type === "Identifier") {
|
|
64
|
-
if (functionNames.has(node.callee.name)) {
|
|
65
|
-
for (const argument of node.arguments) {
|
|
66
|
-
collectFromExpression(
|
|
67
|
-
argument,
|
|
68
|
-
ignoredKeys,
|
|
69
|
-
functionNames,
|
|
70
|
-
literals
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
function extractClassLiteralsFromJsxAttribute(attribute, options) {
|
|
77
|
-
const literals = [];
|
|
78
|
-
const ignoredKeys = new Set(options.ignoredKeys);
|
|
79
|
-
const functionNames = new Set(options.functions);
|
|
80
|
-
if (!attribute.value) {
|
|
81
|
-
return literals;
|
|
82
|
-
}
|
|
83
|
-
if (attribute.value.type === "Literal" && typeof attribute.value.value === "string") {
|
|
84
|
-
literals.push({ value: attribute.value.value, node: attribute.value });
|
|
85
|
-
return literals;
|
|
86
|
-
}
|
|
87
|
-
if (attribute.value.type === "JSXExpressionContainer") {
|
|
88
|
-
collectFromExpression(
|
|
89
|
-
attribute.value.expression,
|
|
90
|
-
ignoredKeys,
|
|
91
|
-
functionNames,
|
|
92
|
-
literals
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
return literals;
|
|
96
|
-
}
|
|
97
|
-
function extractClassLiteralsFromCallExpression(callExpression, options) {
|
|
98
|
-
if (callExpression.callee.type !== "Identifier") {
|
|
99
|
-
return [];
|
|
100
|
-
}
|
|
101
|
-
const functionNames = new Set(options.functions);
|
|
102
|
-
if (!functionNames.has(callExpression.callee.name)) {
|
|
103
|
-
return [];
|
|
104
|
-
}
|
|
105
|
-
const literals = [];
|
|
106
|
-
const ignoredKeys = new Set(options.ignoredKeys);
|
|
107
|
-
for (const argument of callExpression.arguments) {
|
|
108
|
-
collectFromExpression(
|
|
109
|
-
argument,
|
|
110
|
-
ignoredKeys,
|
|
111
|
-
functionNames,
|
|
112
|
-
literals
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
return literals;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const DEFAULT_ATTRIBUTES = ["class", "className"];
|
|
119
|
-
const DEFAULT_FUNCTIONS = [
|
|
120
|
-
"cn",
|
|
121
|
-
"clsx",
|
|
122
|
-
"cva",
|
|
123
|
-
"twMerge",
|
|
124
|
-
"twJoin"
|
|
125
|
-
];
|
|
126
|
-
const DEFAULT_IGNORED_KEYS = [
|
|
127
|
-
"defaultVariants",
|
|
128
|
-
"compoundVariants",
|
|
129
|
-
"compoundSlots"
|
|
130
|
-
];
|
|
131
|
-
function toRegExp(pattern) {
|
|
132
|
-
return new RegExp(pattern);
|
|
133
|
-
}
|
|
134
|
-
function parsePluginSettings(settings) {
|
|
135
|
-
if (!settings?.cssConfigPath) {
|
|
136
|
-
throw new Error(
|
|
137
|
-
"eslint-plugin-tailwind-ts: settings.tailwindTs.cssConfigPath is required (path to your Tailwind v4 CSS entry file)."
|
|
138
|
-
);
|
|
139
|
-
}
|
|
140
|
-
const whitelist = (settings.whitelist ?? []).map(toRegExp);
|
|
141
|
-
return {
|
|
142
|
-
cssConfigPath: settings.cssConfigPath,
|
|
143
|
-
attributes: settings.attributes ?? [...DEFAULT_ATTRIBUTES],
|
|
144
|
-
functions: settings.functions ?? [...DEFAULT_FUNCTIONS],
|
|
145
|
-
whitelist,
|
|
146
|
-
ignoredKeys: settings.ignoredKeys ?? [...DEFAULT_IGNORED_KEYS]
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
function isWhitelisted(className, whitelist) {
|
|
150
|
-
return whitelist.some((pattern) => pattern.test(className));
|
|
151
|
-
}
|
|
152
|
-
function splitClassTokens(classString) {
|
|
153
|
-
return classString.trim().split(/\s+/).filter(Boolean);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const CACHE_TTL_MS = 10 * 60 * 1e3;
|
|
157
|
-
const resultCache = /* @__PURE__ */ new Map();
|
|
158
|
-
const compilerMtimeCache = /* @__PURE__ */ new Map();
|
|
159
|
-
function resolveWorkerPath() {
|
|
160
|
-
const dir = dirname(fileURLToPath(import.meta.url));
|
|
161
|
-
const candidates = [
|
|
162
|
-
join(dir, "../worker/validate.mjs"),
|
|
163
|
-
join(dir, "../../worker/validate.mjs")
|
|
164
|
-
];
|
|
165
|
-
for (const candidate of candidates) {
|
|
166
|
-
if (existsSync(candidate)) {
|
|
167
|
-
return candidate;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
throw new Error("eslint-plugin-tailwind-ts: validation worker not found");
|
|
171
|
-
}
|
|
172
|
-
const runValidate = createSyncFn(resolveWorkerPath());
|
|
173
|
-
function resolveCssConfigPath(cssConfigPath, cwd) {
|
|
174
|
-
return isAbsolute(cssConfigPath) ? cssConfigPath : resolve(cwd, cssConfigPath);
|
|
175
|
-
}
|
|
176
|
-
function getCssMtime(cssConfigPath) {
|
|
177
|
-
return statSync(cssConfigPath).mtimeMs;
|
|
178
|
-
}
|
|
179
|
-
function isCompilerCacheValid(cssConfigPath) {
|
|
180
|
-
const cached = compilerMtimeCache.get(cssConfigPath);
|
|
181
|
-
if (!cached) {
|
|
182
|
-
return false;
|
|
183
|
-
}
|
|
184
|
-
try {
|
|
185
|
-
return cached.mtimeMs === getCssMtime(cssConfigPath);
|
|
186
|
-
} catch {
|
|
187
|
-
return false;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
function touchCompilerCache(cssConfigPath) {
|
|
191
|
-
compilerMtimeCache.set(cssConfigPath, {
|
|
192
|
-
mtimeMs: getCssMtime(cssConfigPath)
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
function invalidateResultCacheForConfig(cssConfigPath) {
|
|
196
|
-
for (const key of resultCache.keys()) {
|
|
197
|
-
if (key.startsWith(`${cssConfigPath}::`)) {
|
|
198
|
-
resultCache.delete(key);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
function validateTailwindClass(request) {
|
|
203
|
-
const resolvedCssPath = resolveCssConfigPath(
|
|
204
|
-
request.cssConfigPath,
|
|
205
|
-
request.cwd
|
|
206
|
-
);
|
|
207
|
-
if (!isCompilerCacheValid(resolvedCssPath)) {
|
|
208
|
-
invalidateResultCacheForConfig(resolvedCssPath);
|
|
209
|
-
touchCompilerCache(resolvedCssPath);
|
|
210
|
-
}
|
|
211
|
-
const cacheKey = `${resolvedCssPath}::${request.className}`;
|
|
212
|
-
const cached = resultCache.get(cacheKey);
|
|
213
|
-
if (cached && cached.expiresAt > Date.now()) {
|
|
214
|
-
return { valid: cached.valid };
|
|
215
|
-
}
|
|
216
|
-
const response = runValidate({
|
|
217
|
-
cssConfigPath: resolvedCssPath,
|
|
218
|
-
className: request.className,
|
|
219
|
-
cwd: request.cwd
|
|
220
|
-
});
|
|
221
|
-
resultCache.set(cacheKey, {
|
|
222
|
-
valid: response.valid,
|
|
223
|
-
expiresAt: Date.now() + CACHE_TTL_MS
|
|
224
|
-
});
|
|
225
|
-
return response;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const RULE_NAME = "no-invalid-classname";
|
|
229
|
-
const noInvalidClassname = ESLintUtils.RuleCreator.withoutDocs({
|
|
230
|
-
meta: {
|
|
231
|
-
type: "problem",
|
|
232
|
-
docs: {
|
|
233
|
-
description: "Disallow invalid or typo Tailwind CSS class names in JSX and utility functions."
|
|
234
|
-
},
|
|
235
|
-
schema: [
|
|
236
|
-
{
|
|
237
|
-
type: "object",
|
|
238
|
-
additionalProperties: false,
|
|
239
|
-
properties: {
|
|
240
|
-
whitelist: {
|
|
241
|
-
type: "array",
|
|
242
|
-
items: { type: "string" }
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
],
|
|
247
|
-
messages: {
|
|
248
|
-
invalidClass: 'Class "{{ className }}" is not a valid Tailwind CSS class.'
|
|
249
|
-
}
|
|
250
|
-
},
|
|
251
|
-
defaultOptions: [{}],
|
|
252
|
-
create(context) {
|
|
253
|
-
const pluginSettings = context.settings.tailwindTs;
|
|
254
|
-
let resolvedSettings;
|
|
255
|
-
try {
|
|
256
|
-
resolvedSettings = parsePluginSettings(pluginSettings);
|
|
257
|
-
} catch (error) {
|
|
258
|
-
const message = error instanceof Error ? error.message : "Invalid plugin settings.";
|
|
259
|
-
context.report({
|
|
260
|
-
loc: { line: 1, column: 0 },
|
|
261
|
-
message
|
|
262
|
-
});
|
|
263
|
-
return {};
|
|
264
|
-
}
|
|
265
|
-
const ruleWhitelist = (context.options[0]?.whitelist ?? []).map(
|
|
266
|
-
(pattern) => new RegExp(pattern)
|
|
267
|
-
);
|
|
268
|
-
const whitelist = [...resolvedSettings.whitelist, ...ruleWhitelist];
|
|
269
|
-
const attributeNames = new Set(resolvedSettings.attributes);
|
|
270
|
-
const cwd = context.cwd ?? process.cwd();
|
|
271
|
-
function reportInvalidClasses(literals) {
|
|
272
|
-
for (const literal of literals) {
|
|
273
|
-
for (const className of splitClassTokens(literal.value)) {
|
|
274
|
-
if (isWhitelisted(className, whitelist)) {
|
|
275
|
-
continue;
|
|
276
|
-
}
|
|
277
|
-
const result = validateTailwindClass({
|
|
278
|
-
cssConfigPath: resolvedSettings.cssConfigPath,
|
|
279
|
-
className,
|
|
280
|
-
cwd
|
|
281
|
-
});
|
|
282
|
-
if (!result.valid) {
|
|
283
|
-
context.report({
|
|
284
|
-
node: literal.node,
|
|
285
|
-
messageId: "invalidClass",
|
|
286
|
-
data: { className }
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
return {
|
|
293
|
-
JSXAttribute(node) {
|
|
294
|
-
if (node.name.type !== "JSXIdentifier" || !attributeNames.has(node.name.name)) {
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
reportInvalidClasses(
|
|
298
|
-
extractClassLiteralsFromJsxAttribute(node, {
|
|
299
|
-
functions: resolvedSettings.functions,
|
|
300
|
-
ignoredKeys: resolvedSettings.ignoredKeys
|
|
301
|
-
})
|
|
302
|
-
);
|
|
303
|
-
},
|
|
304
|
-
CallExpression(node) {
|
|
305
|
-
reportInvalidClasses(
|
|
306
|
-
extractClassLiteralsFromCallExpression(node, {
|
|
307
|
-
functions: resolvedSettings.functions,
|
|
308
|
-
ignoredKeys: resolvedSettings.ignoredKeys
|
|
309
|
-
})
|
|
310
|
-
);
|
|
311
|
-
}
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
const createConfig = (rules) => {
|
|
317
|
-
const result = {};
|
|
318
|
-
for (const [ruleName, value] of Object.entries(rules)) {
|
|
319
|
-
result[`tailwind-ts/${ruleName}`] = value;
|
|
320
|
-
}
|
|
321
|
-
return result;
|
|
322
|
-
};
|
|
323
|
-
const plugin = {
|
|
324
|
-
meta: {
|
|
325
|
-
name: packageJson.name,
|
|
326
|
-
version: packageJson.version
|
|
327
|
-
},
|
|
328
|
-
configs: {
|
|
329
|
-
get recommended() {
|
|
330
|
-
return sharedConfigs.recommended;
|
|
331
|
-
}
|
|
332
|
-
},
|
|
333
|
-
rules: {
|
|
334
|
-
[RULE_NAME]: noInvalidClassname
|
|
335
|
-
}
|
|
336
|
-
};
|
|
337
|
-
const recommendedRules = {
|
|
338
|
-
[RULE_NAME]: "error"
|
|
339
|
-
};
|
|
340
|
-
const configBase = {
|
|
341
|
-
name: "tailwind-ts/base",
|
|
342
|
-
plugins: {
|
|
343
|
-
"tailwind-ts": plugin
|
|
344
|
-
},
|
|
345
|
-
settings: {
|
|
346
|
-
tailwindTs: {}
|
|
347
|
-
},
|
|
348
|
-
files: ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"],
|
|
349
|
-
languageOptions: {
|
|
350
|
-
parserOptions: {
|
|
351
|
-
ecmaVersion: "latest",
|
|
352
|
-
sourceType: "module",
|
|
353
|
-
ecmaFeatures: {
|
|
354
|
-
jsx: true
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
};
|
|
359
|
-
const sharedConfigs = {
|
|
360
|
-
recommended: {
|
|
361
|
-
...configBase,
|
|
362
|
-
name: "tailwind-ts/recommended",
|
|
363
|
-
rules: createConfig(recommendedRules)
|
|
364
|
-
}
|
|
365
|
-
};
|
|
366
|
-
|
|
367
|
-
export { plugin as default };
|
package/lib/worker/validate.mjs
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { readFileSync, statSync } from "node:fs";
|
|
2
|
-
import { dirname } from "node:path";
|
|
3
|
-
import { pathToFileURL } from "node:url";
|
|
4
|
-
import { __unstable__loadDesignSystem } from "@tailwindcss/node";
|
|
5
|
-
|
|
6
|
-
/** @typedef {{ cssConfigPath: string, className: string, cwd: string }} ValidateClassRequest */
|
|
7
|
-
/** @typedef {{ valid: boolean, error?: string }} ValidateClassResponse */
|
|
8
|
-
|
|
9
|
-
/** @type {Map<string, { mtimeMs: number, designSystem: Awaited<ReturnType<typeof __unstable__loadDesignSystem>> }>} */
|
|
10
|
-
const designSystemCache = new Map();
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* @param {string} cssConfigPath
|
|
14
|
-
*/
|
|
15
|
-
async function getDesignSystem(cssConfigPath) {
|
|
16
|
-
const mtimeMs = statSync(cssConfigPath).mtimeMs;
|
|
17
|
-
const cached = designSystemCache.get(cssConfigPath);
|
|
18
|
-
|
|
19
|
-
if (cached && cached.mtimeMs === mtimeMs) {
|
|
20
|
-
return cached.designSystem;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const css = readFileSync(cssConfigPath, "utf8");
|
|
24
|
-
const designSystem = await __unstable__loadDesignSystem(css, {
|
|
25
|
-
base: dirname(cssConfigPath),
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
designSystemCache.set(cssConfigPath, { mtimeMs, designSystem });
|
|
29
|
-
return designSystem;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* @param {ValidateClassRequest} request
|
|
34
|
-
* @returns {Promise<ValidateClassResponse>}
|
|
35
|
-
*/
|
|
36
|
-
async function validateClass(request) {
|
|
37
|
-
try {
|
|
38
|
-
const designSystem = await getDesignSystem(request.cssConfigPath);
|
|
39
|
-
const cssResults = designSystem.candidatesToCss([request.className]);
|
|
40
|
-
const valid = cssResults[0] !== null;
|
|
41
|
-
|
|
42
|
-
return { valid };
|
|
43
|
-
} catch (error) {
|
|
44
|
-
return {
|
|
45
|
-
valid: false,
|
|
46
|
-
error: error instanceof Error ? error.message : String(error),
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* @param {ValidateClassRequest} request
|
|
53
|
-
* @returns {ValidateClassResponse}
|
|
54
|
-
*/
|
|
55
|
-
function run(request) {
|
|
56
|
-
return validateClass(request);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
60
|
-
const { runAsWorker } = await import("synckit");
|
|
61
|
-
|
|
62
|
-
runAsWorker(run);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export { run };
|