@elmeragroup/internal 0.1.1-canary.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/LICENSE +21 -0
- package/NOTICE +62 -0
- package/README.md +56 -0
- package/dist/api-artifacts/model.d.mts +3 -0
- package/dist/api-artifacts/model.mjs +2 -0
- package/dist/api-extractor.d.mts +3 -0
- package/dist/api-extractor.mjs +3 -0
- package/dist/dist-CcEC-Qb_.mjs +558 -0
- package/dist/dist-_NayZK35.mjs +7760 -0
- package/dist/errors-ByXSCE94.mjs +29 -0
- package/dist/index-C9vKRif1.d.mts +815 -0
- package/dist/index.d.mts +53 -0
- package/dist/index.mjs +15 -0
- package/dist/model-1dzGdHi8.d.mts +35 -0
- package/dist/oxlint/anti-slop.d.mts +6 -0
- package/dist/oxlint/anti-slop.mjs +1588 -0
- package/dist/oxlint.d.mts +6 -0
- package/dist/oxlint.mjs +2029 -0
- package/package.json +72 -0
package/dist/oxlint.mjs
ADDED
|
@@ -0,0 +1,2029 @@
|
|
|
1
|
+
|
|
2
|
+
import { defineRule, eslintCompatPlugin } from "@oxlint/plugins";
|
|
3
|
+
//#region ../oxlint-plugin/filename-normalizer.js
|
|
4
|
+
/**
|
|
5
|
+
* @param {string} filename
|
|
6
|
+
*/
|
|
7
|
+
function normalizeFilename(filename) {
|
|
8
|
+
return filename.replaceAll("\\", "/");
|
|
9
|
+
}
|
|
10
|
+
//#endregion
|
|
11
|
+
//#region ../oxlint-plugin/rules/enforce-variant-standard.js
|
|
12
|
+
/**
|
|
13
|
+
* Component entry: src/components/<name>/<name>.tsx
|
|
14
|
+
* @param {string} filename
|
|
15
|
+
*/
|
|
16
|
+
function isComponentEntry(filename) {
|
|
17
|
+
return /(?:^|\/)src\/components\/([^/]+)\/\1\.tsx$/.test(normalizeFilename(filename));
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Colocated recipe module: src/components/<name>/<name>-variants.ts
|
|
21
|
+
* @param {string} filename
|
|
22
|
+
*/
|
|
23
|
+
function isVariantsModule(filename) {
|
|
24
|
+
return /(?:^|\/)src\/components\/([^/]+)\/\1-variants\.ts$/.test(normalizeFilename(filename));
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* @param {import("estree").Node | null | undefined} callee
|
|
28
|
+
*/
|
|
29
|
+
function isTvCall(callee) {
|
|
30
|
+
return callee?.type === "Identifier" && callee.name === "tv";
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* @param {import("estree").ObjectExpression} obj
|
|
34
|
+
* @param {string} name
|
|
35
|
+
*/
|
|
36
|
+
function getObjectProp(obj, name) {
|
|
37
|
+
for (const prop of obj.properties) {
|
|
38
|
+
if (prop.type !== "Property" || prop.computed) continue;
|
|
39
|
+
if ((prop.key.type === "Identifier" ? prop.key.name : prop.key.type === "Literal" ? String(prop.key.value) : null) === name) return prop.value;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A recipe has axes when `variants` is present and not an empty object.
|
|
44
|
+
* Identifiers and spreads count as axes; we cannot see through them.
|
|
45
|
+
*
|
|
46
|
+
* @param {import("estree").ObjectExpression} obj
|
|
47
|
+
*/
|
|
48
|
+
function recipeHasAxes(obj) {
|
|
49
|
+
const variants = getObjectProp(obj, "variants");
|
|
50
|
+
if (!variants) return false;
|
|
51
|
+
if (variants.type !== "ObjectExpression") return true;
|
|
52
|
+
return variants.properties.length > 0;
|
|
53
|
+
}
|
|
54
|
+
var enforce_variant_standard_default = defineRule({
|
|
55
|
+
meta: {
|
|
56
|
+
type: "problem",
|
|
57
|
+
docs: { description: "Enforce tv recipe structure: named recipe, variants/defaultVariants on recipes with axes, VariantProps typing" },
|
|
58
|
+
messages: {
|
|
59
|
+
unnamedRecipe: "tv() recipes must be assigned to a named const (e.g. buttonVariants).",
|
|
60
|
+
inlineObject: "tv() must receive an inline object.",
|
|
61
|
+
missingDefaultVariants: "tv() recipe '{{name}}' must declare defaultVariants when it has a variants axis.",
|
|
62
|
+
missingVariantProps: "Component files that define a tv() recipe with axes must type props with VariantProps<typeof recipe>."
|
|
63
|
+
},
|
|
64
|
+
schema: []
|
|
65
|
+
},
|
|
66
|
+
defaultOptions: [],
|
|
67
|
+
createOnce(context) {
|
|
68
|
+
let shouldCheck = false;
|
|
69
|
+
let requireVariantProps = false;
|
|
70
|
+
/** @type {import("estree").CallExpression[]} */
|
|
71
|
+
const tvCalls = [];
|
|
72
|
+
return {
|
|
73
|
+
Program() {
|
|
74
|
+
const filename = context.filename;
|
|
75
|
+
shouldCheck = isComponentEntry(filename) || isVariantsModule(filename);
|
|
76
|
+
requireVariantProps = isComponentEntry(filename);
|
|
77
|
+
tvCalls.length = 0;
|
|
78
|
+
},
|
|
79
|
+
CallExpression(node) {
|
|
80
|
+
if (!shouldCheck || !isTvCall(node.callee)) return;
|
|
81
|
+
tvCalls.push(node);
|
|
82
|
+
},
|
|
83
|
+
"Program:exit"() {
|
|
84
|
+
if (!shouldCheck || tvCalls.length === 0) return;
|
|
85
|
+
for (const node of tvCalls) {
|
|
86
|
+
const parent = node.parent;
|
|
87
|
+
const named = parent?.type === "VariableDeclarator" && parent.id.type === "Identifier" ? parent.id.name : null;
|
|
88
|
+
if (!named) {
|
|
89
|
+
context.report({
|
|
90
|
+
node,
|
|
91
|
+
messageId: "unnamedRecipe"
|
|
92
|
+
});
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const firstArg = node.arguments[0];
|
|
96
|
+
if (firstArg?.type !== "ObjectExpression") {
|
|
97
|
+
context.report({
|
|
98
|
+
node,
|
|
99
|
+
messageId: "inlineObject"
|
|
100
|
+
});
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (recipeHasAxes(firstArg) && !getObjectProp(firstArg, "defaultVariants")) context.report({
|
|
104
|
+
node,
|
|
105
|
+
messageId: "missingDefaultVariants",
|
|
106
|
+
data: { name: named }
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
const anyHasAxes = tvCalls.some((call) => {
|
|
110
|
+
const arg = call.arguments[0];
|
|
111
|
+
return arg?.type === "ObjectExpression" && recipeHasAxes(arg);
|
|
112
|
+
});
|
|
113
|
+
if (requireVariantProps && anyHasAxes && !context.sourceCode.getText().includes("VariantProps")) context.report({
|
|
114
|
+
loc: tvCalls[0]?.loc,
|
|
115
|
+
messageId: "missingVariantProps"
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
//#endregion
|
|
122
|
+
//#region ../oxlint-plugin/rules/facade-reexport-grammar.js
|
|
123
|
+
/**
|
|
124
|
+
* Facades are `src/<name>.ts(x)` and `src/react-aria/<name>.ts(x)`.
|
|
125
|
+
* The generated root barrel may use `export *` and is excluded.
|
|
126
|
+
* @param {string} filename
|
|
127
|
+
*/
|
|
128
|
+
function isEntryFacadeFile(filename) {
|
|
129
|
+
const posix = normalizeFilename(filename);
|
|
130
|
+
if (posix.endsWith("/src/index.ts") || posix.endsWith("/src/index.tsx")) return false;
|
|
131
|
+
if (/\/src\/[a-z0-9-]+\.tsx?$/.test(posix)) return true;
|
|
132
|
+
return /\/src\/react-aria\/[a-z0-9-]+\.tsx?$/.test(posix);
|
|
133
|
+
}
|
|
134
|
+
var facade_reexport_grammar_default = defineRule({
|
|
135
|
+
meta: {
|
|
136
|
+
type: "problem",
|
|
137
|
+
docs: { description: "Require src/<name>.ts and src/react-aria/<name>.ts facades to be explicit named re-exports only" },
|
|
138
|
+
messages: { grammar: "Entry facades must be explicit named re-exports only: no export *, no local declarations, no directives." },
|
|
139
|
+
schema: []
|
|
140
|
+
},
|
|
141
|
+
defaultOptions: [],
|
|
142
|
+
createOnce(context) {
|
|
143
|
+
return { Program(program) {
|
|
144
|
+
if (!isEntryFacadeFile(context.filename)) return;
|
|
145
|
+
for (const statement of program.body) {
|
|
146
|
+
if (statement.type === "ExportNamedDeclaration") {
|
|
147
|
+
if (statement.declaration != null || statement.source == null) context.report({
|
|
148
|
+
node: statement,
|
|
149
|
+
messageId: "grammar"
|
|
150
|
+
});
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
context.report({
|
|
154
|
+
node: statement,
|
|
155
|
+
messageId: "grammar"
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
} };
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
//#endregion
|
|
162
|
+
//#region ../oxlint-plugin/rules/no-field-part-jsx.js
|
|
163
|
+
const BANNED_PARTS = /* @__PURE__ */ new Set([
|
|
164
|
+
"Label",
|
|
165
|
+
"Description",
|
|
166
|
+
"Error",
|
|
167
|
+
"Root",
|
|
168
|
+
"Set",
|
|
169
|
+
"Legend"
|
|
170
|
+
]);
|
|
171
|
+
const LABELED_COMPOSITES = [
|
|
172
|
+
"/src/components/text-field/text-field.tsx",
|
|
173
|
+
"/src/components/number-field/number-field.tsx",
|
|
174
|
+
"/src/components/textarea-field/textarea-field.tsx",
|
|
175
|
+
"/src/components/phone-number-field/phone-number-field.tsx",
|
|
176
|
+
"/src/components/checkbox/checkbox.tsx",
|
|
177
|
+
"/src/components/radio-group/radio-group.tsx"
|
|
178
|
+
];
|
|
179
|
+
/**
|
|
180
|
+
* @param {string} filename
|
|
181
|
+
*/
|
|
182
|
+
function isLabeledComposite(filename) {
|
|
183
|
+
const normalized = normalizeFilename(filename);
|
|
184
|
+
return LABELED_COMPOSITES.some((suffix) => normalized.endsWith(suffix));
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* @param {import("estree").Node | null | undefined} nameNode
|
|
188
|
+
* @returns {string | null}
|
|
189
|
+
*/
|
|
190
|
+
function fieldPartName(nameNode) {
|
|
191
|
+
if (nameNode?.type !== "JSXMemberExpression") return null;
|
|
192
|
+
const object = nameNode.object;
|
|
193
|
+
const property = nameNode.property;
|
|
194
|
+
if (object?.type !== "JSXIdentifier" || object.name !== "Field") return null;
|
|
195
|
+
if (property?.type !== "JSXIdentifier") return null;
|
|
196
|
+
return property.name;
|
|
197
|
+
}
|
|
198
|
+
var no_field_part_jsx_default = defineRule({
|
|
199
|
+
meta: {
|
|
200
|
+
type: "problem",
|
|
201
|
+
docs: { description: "Forbid Field.Label/Description/Error/Root/Set/Legend JSX in labeled composites that must go through FieldFrame" },
|
|
202
|
+
messages: { fieldPart: "Labeled composites render <Field.{{part}}> through FieldFrame. Do not reopen that markup here." },
|
|
203
|
+
schema: []
|
|
204
|
+
},
|
|
205
|
+
defaultOptions: [],
|
|
206
|
+
createOnce(context) {
|
|
207
|
+
let skipFile = true;
|
|
208
|
+
return {
|
|
209
|
+
Program() {
|
|
210
|
+
skipFile = !isLabeledComposite(context.filename);
|
|
211
|
+
},
|
|
212
|
+
JSXOpeningElement(node) {
|
|
213
|
+
if (skipFile) return;
|
|
214
|
+
const part = fieldPartName(node.name);
|
|
215
|
+
if (part === null || !BANNED_PARTS.has(part)) return;
|
|
216
|
+
context.report({
|
|
217
|
+
node,
|
|
218
|
+
messageId: "fieldPart",
|
|
219
|
+
data: { part }
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
//#endregion
|
|
226
|
+
//#region ../oxlint-plugin/extract-strings.js
|
|
227
|
+
/**
|
|
228
|
+
* Collect string literals from an ESTree node (class names, tv/cn args).
|
|
229
|
+
* Lifted from kumo lint helpers (MIT, Copyright (c) 2026 Cloudflare, Inc.).
|
|
230
|
+
*
|
|
231
|
+
* @param {import("estree").Node | null | undefined} node
|
|
232
|
+
* @returns {string[]}
|
|
233
|
+
*/
|
|
234
|
+
function extractStrings(node) {
|
|
235
|
+
if (!node) return [];
|
|
236
|
+
const out = [];
|
|
237
|
+
switch (node.type) {
|
|
238
|
+
case "Literal":
|
|
239
|
+
if (typeof node.value === "string") out.push(node.value);
|
|
240
|
+
break;
|
|
241
|
+
case "TemplateLiteral":
|
|
242
|
+
for (const q of node.quasis) if (typeof q.value.cooked === "string") out.push(q.value.cooked);
|
|
243
|
+
break;
|
|
244
|
+
case "BinaryExpression":
|
|
245
|
+
if (node.operator === "+") {
|
|
246
|
+
out.push(...extractStrings(node.left));
|
|
247
|
+
out.push(...extractStrings(node.right));
|
|
248
|
+
}
|
|
249
|
+
break;
|
|
250
|
+
case "ArrayExpression":
|
|
251
|
+
for (const el of node.elements) if (el) out.push(...extractStrings(el));
|
|
252
|
+
break;
|
|
253
|
+
case "ObjectExpression":
|
|
254
|
+
for (const prop of node.properties) if (prop.type === "Property") {
|
|
255
|
+
out.push(...extractStrings(prop.key));
|
|
256
|
+
out.push(...extractStrings(prop.value));
|
|
257
|
+
}
|
|
258
|
+
break;
|
|
259
|
+
case "CallExpression":
|
|
260
|
+
for (const arg of node.arguments) {
|
|
261
|
+
if (arg.type === "SpreadElement") continue;
|
|
262
|
+
out.push(...extractStrings(arg));
|
|
263
|
+
}
|
|
264
|
+
break;
|
|
265
|
+
case "ConditionalExpression":
|
|
266
|
+
out.push(...extractStrings(node.consequent));
|
|
267
|
+
out.push(...extractStrings(node.alternate));
|
|
268
|
+
out.push(...extractStrings(node.test));
|
|
269
|
+
break;
|
|
270
|
+
case "UnaryExpression":
|
|
271
|
+
out.push(...extractStrings(node.argument));
|
|
272
|
+
break;
|
|
273
|
+
case "LogicalExpression":
|
|
274
|
+
out.push(...extractStrings(node.left));
|
|
275
|
+
out.push(...extractStrings(node.right));
|
|
276
|
+
break;
|
|
277
|
+
case "JSXText":
|
|
278
|
+
out.push(node.value);
|
|
279
|
+
break;
|
|
280
|
+
case "JSXExpressionContainer": out.push(...extractStrings(node.expression));
|
|
281
|
+
}
|
|
282
|
+
return out;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* @param {import("estree").Node | null | undefined} callee
|
|
286
|
+
* @param {string} name
|
|
287
|
+
*/
|
|
288
|
+
function isNamedCall(callee, name) {
|
|
289
|
+
return callee?.type === "Identifier" && callee.name === name;
|
|
290
|
+
}
|
|
291
|
+
//#endregion
|
|
292
|
+
//#region ../oxlint-plugin/rules/no-hardcoded-density-metrics.js
|
|
293
|
+
const CONTROL_VAR_RE = /--control-(?:h|px-icon|px|gap)-|--control-(?:text|leading)\b/;
|
|
294
|
+
const KEYWORD_BOX = /* @__PURE__ */ new Set([
|
|
295
|
+
"auto",
|
|
296
|
+
"full",
|
|
297
|
+
"min",
|
|
298
|
+
"max",
|
|
299
|
+
"fit",
|
|
300
|
+
"screen",
|
|
301
|
+
"svh",
|
|
302
|
+
"lvh",
|
|
303
|
+
"dvh",
|
|
304
|
+
"svw",
|
|
305
|
+
"lvw",
|
|
306
|
+
"dvw",
|
|
307
|
+
"px",
|
|
308
|
+
"lh",
|
|
309
|
+
"none",
|
|
310
|
+
"svmin",
|
|
311
|
+
"lvmin",
|
|
312
|
+
"dvmin",
|
|
313
|
+
"svmax",
|
|
314
|
+
"lvmax",
|
|
315
|
+
"dvmax"
|
|
316
|
+
]);
|
|
317
|
+
const ICON_GLYPH_SIZES = /* @__PURE__ */ new Set(["3", "4"]);
|
|
318
|
+
const TYPE_SCALE = /* @__PURE__ */ new Set([
|
|
319
|
+
"xs",
|
|
320
|
+
"sm",
|
|
321
|
+
"base",
|
|
322
|
+
"lg",
|
|
323
|
+
"xl",
|
|
324
|
+
"2xl",
|
|
325
|
+
"3xl",
|
|
326
|
+
"4xl",
|
|
327
|
+
"5xl",
|
|
328
|
+
"6xl",
|
|
329
|
+
"7xl",
|
|
330
|
+
"8xl",
|
|
331
|
+
"9xl"
|
|
332
|
+
]);
|
|
333
|
+
const MD_LG_KEYS = /* @__PURE__ */ new Set([
|
|
334
|
+
"md",
|
|
335
|
+
"lg",
|
|
336
|
+
"default"
|
|
337
|
+
]);
|
|
338
|
+
const BUTTON_SIZE_KEYS = /* @__PURE__ */ new Set([
|
|
339
|
+
"default",
|
|
340
|
+
"xs",
|
|
341
|
+
"sm",
|
|
342
|
+
"md",
|
|
343
|
+
"lg",
|
|
344
|
+
"icon",
|
|
345
|
+
"icon-xxs",
|
|
346
|
+
"icon-xs",
|
|
347
|
+
"icon-sm",
|
|
348
|
+
"icon-inline",
|
|
349
|
+
"icon-lg"
|
|
350
|
+
]);
|
|
351
|
+
/**
|
|
352
|
+
* @param {import("estree").ObjectExpression} obj
|
|
353
|
+
* @param {string} name
|
|
354
|
+
* @returns {import("estree").Node | null}
|
|
355
|
+
*/
|
|
356
|
+
function objectPropValue(obj, name) {
|
|
357
|
+
for (const prop of obj.properties) {
|
|
358
|
+
if (prop.type !== "Property") continue;
|
|
359
|
+
if ((prop.key.type === "Identifier" ? prop.key.name : prop.key.type === "Literal" ? prop.key.value : null) === name) return prop.value;
|
|
360
|
+
}
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* @param {import("estree").Property} prop
|
|
365
|
+
* @returns {string | null}
|
|
366
|
+
*/
|
|
367
|
+
function propertyName(prop) {
|
|
368
|
+
if (prop.computed && prop.key.type !== "Literal") return null;
|
|
369
|
+
if (prop.key.type === "Identifier") return prop.key.name;
|
|
370
|
+
if (prop.key.type === "Literal" && typeof prop.key.value === "string") return prop.key.value;
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* @param {string} str
|
|
375
|
+
* @param {number} start
|
|
376
|
+
*/
|
|
377
|
+
function matchingBracket(str, start) {
|
|
378
|
+
if (str[start] !== "[") return -1;
|
|
379
|
+
let depth = 0;
|
|
380
|
+
for (let i = start; i < str.length; i++) {
|
|
381
|
+
const ch = str[i];
|
|
382
|
+
if (ch === "[") depth += 1;
|
|
383
|
+
else if (ch === "]") {
|
|
384
|
+
depth -= 1;
|
|
385
|
+
if (depth === 0) return i;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return -1;
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Strip Tailwind variant prefixes, including arbitrary and data variants.
|
|
392
|
+
* @param {string} className
|
|
393
|
+
*/
|
|
394
|
+
function stripVariantPrefixes(className) {
|
|
395
|
+
let rest = className;
|
|
396
|
+
while (rest.length > 0) {
|
|
397
|
+
if (rest.startsWith("[")) {
|
|
398
|
+
const end = matchingBracket(rest, 0);
|
|
399
|
+
if (end !== -1 && rest[end + 1] === ":") {
|
|
400
|
+
rest = rest.slice(end + 2);
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
if (rest.startsWith("*:") || rest.startsWith("**:")) {
|
|
406
|
+
rest = rest.slice(rest.indexOf(":") + 1);
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
const nameMatch = /^[a-zA-Z@][\w-]*(?:\/[\w-]+)?/.exec(rest);
|
|
410
|
+
if (!nameMatch) break;
|
|
411
|
+
let i = nameMatch[0].length;
|
|
412
|
+
if (rest[i] === "[") {
|
|
413
|
+
const end = matchingBracket(rest, i);
|
|
414
|
+
if (end === -1) break;
|
|
415
|
+
i = end + 1;
|
|
416
|
+
}
|
|
417
|
+
if (rest[i] === ":") {
|
|
418
|
+
rest = rest.slice(i + 1);
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
return rest;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* @param {string} className
|
|
427
|
+
*/
|
|
428
|
+
function isDescendantTargeted(className) {
|
|
429
|
+
return /\[[^\]]*&/.test(className);
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* @param {string} utility
|
|
433
|
+
*/
|
|
434
|
+
function stripImportant(utility) {
|
|
435
|
+
let next = utility;
|
|
436
|
+
if (next.startsWith("!")) next = next.slice(1);
|
|
437
|
+
if (next.endsWith("!")) next = next.slice(0, -1);
|
|
438
|
+
return next;
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* @param {string} utility
|
|
442
|
+
*/
|
|
443
|
+
function readsDensityVariable(utility) {
|
|
444
|
+
return CONTROL_VAR_RE.test(utility);
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* @param {string} sizeKey
|
|
448
|
+
*/
|
|
449
|
+
function isMdLgRung(sizeKey) {
|
|
450
|
+
return MD_LG_KEYS.has(sizeKey);
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* @param {string} value
|
|
454
|
+
*/
|
|
455
|
+
function isKeywordBox(value) {
|
|
456
|
+
return KEYWORD_BOX.has(value);
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* @param {string} utility
|
|
460
|
+
* @param {string} prefix
|
|
461
|
+
* @returns {string | null}
|
|
462
|
+
*/
|
|
463
|
+
function valueAfter(utility, prefix) {
|
|
464
|
+
if (!utility.startsWith(prefix)) return null;
|
|
465
|
+
return utility.slice(prefix.length);
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* @param {string} utility
|
|
469
|
+
* @returns {string | null}
|
|
470
|
+
*/
|
|
471
|
+
function boxFamily(utility) {
|
|
472
|
+
const height = valueAfter(utility, "h-");
|
|
473
|
+
if (height !== null) {
|
|
474
|
+
if (!height || isKeywordBox(height)) return null;
|
|
475
|
+
return "height";
|
|
476
|
+
}
|
|
477
|
+
const square = valueAfter(utility, "size-");
|
|
478
|
+
if (square !== null) {
|
|
479
|
+
if (!square || isKeywordBox(square) || ICON_GLYPH_SIZES.has(square)) return null;
|
|
480
|
+
return "height";
|
|
481
|
+
}
|
|
482
|
+
const gapX = valueAfter(utility, "gap-x-");
|
|
483
|
+
if (gapX !== null) return gapX && !isKeywordBox(gapX) ? "gap" : null;
|
|
484
|
+
if (valueAfter(utility, "gap-y-") !== null) return null;
|
|
485
|
+
const gap = valueAfter(utility, "gap-");
|
|
486
|
+
if (gap !== null) return gap && !isKeywordBox(gap) ? "gap" : null;
|
|
487
|
+
const px = valueAfter(utility, "px-");
|
|
488
|
+
if (px !== null) return px && !isKeywordBox(px) ? "inline padding" : null;
|
|
489
|
+
const pl = valueAfter(utility, "pl-");
|
|
490
|
+
if (pl !== null) return pl && !isKeywordBox(pl) ? "icon-edge padding" : null;
|
|
491
|
+
const pr = valueAfter(utility, "pr-");
|
|
492
|
+
if (pr !== null) return pr && !isKeywordBox(pr) ? "icon-edge padding" : null;
|
|
493
|
+
const ps = valueAfter(utility, "ps-");
|
|
494
|
+
if (ps !== null) return ps && !isKeywordBox(ps) ? "icon-edge padding" : null;
|
|
495
|
+
const pe = valueAfter(utility, "pe-");
|
|
496
|
+
if (pe !== null) return pe && !isKeywordBox(pe) ? "icon-edge padding" : null;
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* @param {string} utility
|
|
501
|
+
* @returns {string | null}
|
|
502
|
+
*/
|
|
503
|
+
function typeFamily(utility) {
|
|
504
|
+
if (utility.startsWith("[font-size:") || utility.startsWith("[line-height:")) return "type";
|
|
505
|
+
if (utility.startsWith("leading-")) return "type";
|
|
506
|
+
const text = valueAfter(utility, "text-");
|
|
507
|
+
if (text === null) return null;
|
|
508
|
+
const scale = text.split("/")[0];
|
|
509
|
+
if (scale && (TYPE_SCALE.has(scale) || scale.startsWith("[") || scale.startsWith("("))) return "type";
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* @param {string} className
|
|
514
|
+
*/
|
|
515
|
+
function isControlBoxHeightClass(className) {
|
|
516
|
+
if (isDescendantTargeted(className)) return false;
|
|
517
|
+
const utility = stripImportant(stripVariantPrefixes(className));
|
|
518
|
+
if (!utility) return false;
|
|
519
|
+
if (utility.includes("--control-h-")) return true;
|
|
520
|
+
const height = valueAfter(utility, "h-");
|
|
521
|
+
if (height !== null) return Boolean(height) && !isKeywordBox(height);
|
|
522
|
+
const square = valueAfter(utility, "size-");
|
|
523
|
+
if (square === null) return false;
|
|
524
|
+
if (ICON_GLYPH_SIZES.has(square) || isKeywordBox(square)) return false;
|
|
525
|
+
return Boolean(square);
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* @param {string} utility
|
|
529
|
+
*/
|
|
530
|
+
function isOpticalArbitrary(utility) {
|
|
531
|
+
return /^(?:h|w|size|min-h|min-w|px|pl|pr|ps|pe|gap(?:-[xy])?)-\[\d+(?:\.\d+)?px\]$/.test(utility);
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* @param {string} className
|
|
535
|
+
*/
|
|
536
|
+
function isDataSizeToken(className) {
|
|
537
|
+
return /(?:^|:)data-\[size=/.test(className) && !className.includes("has-data-[size=");
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* @param {string} className
|
|
541
|
+
* @param {boolean} checkType
|
|
542
|
+
* @returns {string | null}
|
|
543
|
+
*/
|
|
544
|
+
function densityOwnedFamily(className, checkType) {
|
|
545
|
+
if (isDescendantTargeted(className)) return null;
|
|
546
|
+
const utility = stripImportant(stripVariantPrefixes(className));
|
|
547
|
+
if (!utility) return null;
|
|
548
|
+
if (readsDensityVariable(utility)) return null;
|
|
549
|
+
if (/\(--[\w-]+\)/.test(utility) || /var\(--/.test(utility)) return null;
|
|
550
|
+
if (/^(?:h|w|size|min-h|min-w|px|pl|pr|ps|pe|gap(?:-[xy])?)-\[/.test(utility)) return null;
|
|
551
|
+
if (isOpticalArbitrary(utility)) return null;
|
|
552
|
+
const box = boxFamily(utility);
|
|
553
|
+
if (box) return box;
|
|
554
|
+
if (!checkType) return null;
|
|
555
|
+
return typeFamily(utility);
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* @param {string} str
|
|
559
|
+
* @returns {string[]}
|
|
560
|
+
*/
|
|
561
|
+
function classTokens$1(str) {
|
|
562
|
+
return str.split(/\s+/).filter(Boolean);
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Class tokens from a tv/cn/record arm, excluding `data-[size=…]` tokens
|
|
566
|
+
* which are reported from the literal/template visitors instead.
|
|
567
|
+
*
|
|
568
|
+
* @param {import("estree").Node | null | undefined} node
|
|
569
|
+
* @returns {string[]}
|
|
570
|
+
*/
|
|
571
|
+
function recipeTokens(node) {
|
|
572
|
+
return extractStrings(node).flatMap(classTokens$1).filter((token) => !isDataSizeToken(token));
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* @param {import("estree").Node | null | undefined} node
|
|
576
|
+
* @param {string} name
|
|
577
|
+
*/
|
|
578
|
+
function isInsideNamedCall(node, name) {
|
|
579
|
+
for (let current = node?.parent; current; current = current.parent) if (current.type === "CallExpression" && isNamedCall(current.callee, name)) return true;
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* @param {string[]} tokens
|
|
584
|
+
*/
|
|
585
|
+
function tokensHaveControlHeightPin(tokens) {
|
|
586
|
+
return tokens.some((token) => token.includes("--control-h-"));
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* @param {string[]} tokens
|
|
590
|
+
*/
|
|
591
|
+
function tokensHaveControlVar(tokens) {
|
|
592
|
+
return tokens.some((token) => CONTROL_VAR_RE.test(token));
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* @param {string[]} tokens
|
|
596
|
+
*/
|
|
597
|
+
function checkTypeForTokens(tokens) {
|
|
598
|
+
return tokens.some((token) => /--control-h-(?:md|lg)\b/.test(token) || /--control-text\b/.test(token));
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* @param {import("estree").ObjectExpression} obj
|
|
602
|
+
* @returns {Array<{ key: string; value: import("estree").Node }> | null}
|
|
603
|
+
*/
|
|
604
|
+
function recordArms(obj) {
|
|
605
|
+
/** @type {Array<{ key: string; value: import("estree").Node }>} */
|
|
606
|
+
const arms = [];
|
|
607
|
+
for (const prop of obj.properties) {
|
|
608
|
+
if (prop.type !== "Property") return null;
|
|
609
|
+
const key = propertyName(prop);
|
|
610
|
+
if (key === null) return null;
|
|
611
|
+
arms.push({
|
|
612
|
+
key,
|
|
613
|
+
value: prop.value
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
return arms;
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* @param {import("estree").ObjectExpression} obj
|
|
620
|
+
*/
|
|
621
|
+
function isButtonSizeKeyedRecord(obj) {
|
|
622
|
+
const arms = recordArms(obj);
|
|
623
|
+
if (arms === null || arms.length === 0) return false;
|
|
624
|
+
return arms.every((arm) => BUTTON_SIZE_KEYS.has(arm.key));
|
|
625
|
+
}
|
|
626
|
+
var no_hardcoded_density_metrics_default = defineRule({
|
|
627
|
+
meta: {
|
|
628
|
+
type: "suggestion",
|
|
629
|
+
docs: { description: "Warn when control-box recipes, data-[size] class strings, cn() strings, tv slots, or Button-size-keyed records hardcode density-owned metrics instead of reading --control-* variables" },
|
|
630
|
+
messages: { hardcodedMetric: "Hardcoded density-owned {{family}} `{{utility}}`. Read the matching `--control-*` variable instead." },
|
|
631
|
+
schema: []
|
|
632
|
+
},
|
|
633
|
+
defaultOptions: [],
|
|
634
|
+
createOnce(context) {
|
|
635
|
+
let fileHasControlH = false;
|
|
636
|
+
/**
|
|
637
|
+
* @param {import("estree").Node} node
|
|
638
|
+
* @param {string[]} tokens
|
|
639
|
+
* @param {boolean} checkType
|
|
640
|
+
*/
|
|
641
|
+
function reportTokens(node, tokens, checkType) {
|
|
642
|
+
for (const token of tokens) {
|
|
643
|
+
const family = densityOwnedFamily(token, checkType);
|
|
644
|
+
if (!family) continue;
|
|
645
|
+
context.report({
|
|
646
|
+
node,
|
|
647
|
+
messageId: "hardcodedMetric",
|
|
648
|
+
data: {
|
|
649
|
+
family,
|
|
650
|
+
utility: token
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* @param {import("estree").Node} node
|
|
657
|
+
* @param {string} value
|
|
658
|
+
*/
|
|
659
|
+
function reportDataSizeLiterals(node, value) {
|
|
660
|
+
reportTokens(node, classTokens$1(value).filter(isDataSizeToken), false);
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* @param {import("estree").CallExpression} node
|
|
664
|
+
*/
|
|
665
|
+
function reportCnLiterals(node) {
|
|
666
|
+
if (isInsideNamedCall(node, "tv")) return;
|
|
667
|
+
const tokens = recipeTokens(node);
|
|
668
|
+
if (!fileHasControlH && !tokensHaveControlHeightPin(tokens)) return;
|
|
669
|
+
reportTokens(node, tokens, checkTypeForTokens(tokens));
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* @param {import("estree").ObjectExpression} recipe
|
|
673
|
+
*/
|
|
674
|
+
function reportTvSlots(recipe) {
|
|
675
|
+
const slots = objectPropValue(recipe, "slots");
|
|
676
|
+
if (slots?.type !== "ObjectExpression") return;
|
|
677
|
+
for (const prop of slots.properties) {
|
|
678
|
+
if (prop.type !== "Property") continue;
|
|
679
|
+
const tokens = recipeTokens(prop.value);
|
|
680
|
+
if (!fileHasControlH && !tokensHaveControlHeightPin(tokens) && !tokensHaveControlVar(tokens)) continue;
|
|
681
|
+
reportTokens(prop.value, tokens, checkTypeForTokens(tokens));
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* @param {import("estree").ObjectExpression} node
|
|
686
|
+
*/
|
|
687
|
+
function reportSizeKeyedRecord(node) {
|
|
688
|
+
if (isInsideNamedCall(node, "tv")) return;
|
|
689
|
+
if (!isButtonSizeKeyedRecord(node)) return;
|
|
690
|
+
const arms = recordArms(node);
|
|
691
|
+
if (arms === null) return;
|
|
692
|
+
const groups = arms.map((arm) => ({
|
|
693
|
+
key: arm.key,
|
|
694
|
+
node: arm.value,
|
|
695
|
+
tokens: recipeTokens(arm.value)
|
|
696
|
+
}));
|
|
697
|
+
const allTokens = groups.flatMap((group) => group.tokens);
|
|
698
|
+
if (!fileHasControlH && !tokensHaveControlHeightPin(allTokens) && !allTokens.some(isControlBoxHeightClass)) return;
|
|
699
|
+
for (const group of groups) reportTokens(group.node, group.tokens, isMdLgRung(group.key));
|
|
700
|
+
}
|
|
701
|
+
return {
|
|
702
|
+
Program() {
|
|
703
|
+
fileHasControlH = context.sourceCode.getText().includes("--control-h-");
|
|
704
|
+
},
|
|
705
|
+
Literal(node) {
|
|
706
|
+
if (typeof node.value === "string") reportDataSizeLiterals(node, node.value);
|
|
707
|
+
},
|
|
708
|
+
TemplateLiteral(node) {
|
|
709
|
+
for (const quasi of node.quasis) if (typeof quasi.value.cooked === "string") reportDataSizeLiterals(quasi, quasi.value.cooked);
|
|
710
|
+
},
|
|
711
|
+
ObjectExpression(node) {
|
|
712
|
+
reportSizeKeyedRecord(node);
|
|
713
|
+
},
|
|
714
|
+
CallExpression(node) {
|
|
715
|
+
if (isNamedCall(node.callee, "cn")) {
|
|
716
|
+
reportCnLiterals(node);
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
if (!isNamedCall(node.callee, "tv")) return;
|
|
720
|
+
if (node.arguments.length === 0) return;
|
|
721
|
+
const recipe = node.arguments[0];
|
|
722
|
+
if (recipe.type !== "ObjectExpression") return;
|
|
723
|
+
const variants = objectPropValue(recipe, "variants");
|
|
724
|
+
const size = variants?.type === "ObjectExpression" ? objectPropValue(variants, "size") : null;
|
|
725
|
+
if (size?.type === "ObjectExpression") for (const prop of size.properties) {
|
|
726
|
+
if (prop.type !== "Property") continue;
|
|
727
|
+
const sizeKey = propertyName(prop);
|
|
728
|
+
if (sizeKey === null) continue;
|
|
729
|
+
const tokens = recipeTokens(prop.value);
|
|
730
|
+
if (!tokens.some(isControlBoxHeightClass)) continue;
|
|
731
|
+
const checkType = isMdLgRung(sizeKey);
|
|
732
|
+
reportTokens(prop.value, tokens, checkType);
|
|
733
|
+
}
|
|
734
|
+
else {
|
|
735
|
+
/** @type {Array<{ node: import("estree").Node; tokens: string[] }>} */
|
|
736
|
+
const groups = [];
|
|
737
|
+
const base = objectPropValue(recipe, "base");
|
|
738
|
+
if (base) groups.push({
|
|
739
|
+
node: base,
|
|
740
|
+
tokens: recipeTokens(base)
|
|
741
|
+
});
|
|
742
|
+
const box = variants?.type === "ObjectExpression" ? objectPropValue(variants, "box") : null;
|
|
743
|
+
if (box?.type === "ObjectExpression") for (const arm of box.properties) {
|
|
744
|
+
if (arm.type !== "Property") continue;
|
|
745
|
+
groups.push({
|
|
746
|
+
node: arm.value,
|
|
747
|
+
tokens: recipeTokens(arm.value)
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
if (groups.some((group) => group.tokens.some(isControlBoxHeightClass))) for (const group of groups) reportTokens(group.node, group.tokens, false);
|
|
751
|
+
}
|
|
752
|
+
reportTvSlots(recipe);
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
});
|
|
757
|
+
//#endregion
|
|
758
|
+
//#region ../oxlint-plugin/rules/no-internal-dynamic-import.js
|
|
759
|
+
var no_internal_dynamic_import_default = defineRule({
|
|
760
|
+
createOnce(context) {
|
|
761
|
+
return {
|
|
762
|
+
/** @param {import("estree").ImportExpression} node */
|
|
763
|
+
ImportExpression(node) {
|
|
764
|
+
context.report({
|
|
765
|
+
loc: node.loc,
|
|
766
|
+
messageId: "noDynamicImport"
|
|
767
|
+
});
|
|
768
|
+
} };
|
|
769
|
+
},
|
|
770
|
+
meta: {
|
|
771
|
+
type: "problem",
|
|
772
|
+
docs: { description: "Disallow dynamic import() in library source; apps own code splitting" },
|
|
773
|
+
schema: [],
|
|
774
|
+
messages: { noDynamicImport: "The library never lazy-loads internally. Dynamic import() is forbidden in packages/ui/src." }
|
|
775
|
+
}
|
|
776
|
+
});
|
|
777
|
+
//#endregion
|
|
778
|
+
//#region ../oxlint-plugin/rules/no-local-focus-ring.js
|
|
779
|
+
const ALLOWED_UTILS_SUFFIX = "/src/styles/utils.ts";
|
|
780
|
+
const FOCUS_RING_RE = /(?:^|[\s"'`[])(?:[\w-[\]]+:)*(?:focus(?:-visible|-within)?|has-focus|in-focus|data-\[focus(?:-visible)?\]):(?:[\w-[\]]+:)*ring(?:-|\b)/;
|
|
781
|
+
const OUTLINE_SUPPRESSION_RE = /(?:^|[\s"'`])(?:[\w-[\]./*]+?:)*outline-(?:none|hidden)(?:\s|"|'|`|$)/;
|
|
782
|
+
const FOCUS_WITHIN_BORDER_RE = /(?:^|[\s"'`])(?:[\w-[\]./*]+?:)*(?:group-|peer-)?focus-within(?:\/[\w-]+)?:(?:[\w-[\]./*]+?:)*border(?:-|\b)/;
|
|
783
|
+
/**
|
|
784
|
+
* @param {string} filename
|
|
785
|
+
*/
|
|
786
|
+
function isFocusRingUtils(filename) {
|
|
787
|
+
return normalizeFilename(filename).endsWith(ALLOWED_UTILS_SUFFIX);
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* @param {import("estree").Node | null | undefined} node
|
|
791
|
+
*/
|
|
792
|
+
function isFocusVisibleIdentifier(node) {
|
|
793
|
+
return node?.type === "Identifier" && node.name === "isFocusVisible";
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* @param {string} str
|
|
797
|
+
*/
|
|
798
|
+
function hasBareRingClass(str) {
|
|
799
|
+
return /(?:^|[\s"'`])(?:[\w-]+:)*ring(?:-\S+)?/.test(str);
|
|
800
|
+
}
|
|
801
|
+
var no_local_focus_ring_default = defineRule({
|
|
802
|
+
meta: {
|
|
803
|
+
type: "problem",
|
|
804
|
+
docs: { description: "Forbid focus-state ring classes, outline-(none|hidden), and focus-within border colours outside packages/ui/src/styles/utils.ts" },
|
|
805
|
+
messages: { localFocusRing: "Focus-state rings must come from the package-private focusRing recipe in src/styles/utils.ts." },
|
|
806
|
+
schema: []
|
|
807
|
+
},
|
|
808
|
+
defaultOptions: [],
|
|
809
|
+
createOnce(context) {
|
|
810
|
+
/**
|
|
811
|
+
* @param {import("estree").Node} node
|
|
812
|
+
* @param {string[]} collected
|
|
813
|
+
*/
|
|
814
|
+
function reportFocusRingStrings(node, collected) {
|
|
815
|
+
for (const value of collected) if (FOCUS_RING_RE.test(value) || OUTLINE_SUPPRESSION_RE.test(value) || FOCUS_WITHIN_BORDER_RE.test(value)) {
|
|
816
|
+
context.report({
|
|
817
|
+
node,
|
|
818
|
+
messageId: "localFocusRing"
|
|
819
|
+
});
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* @param {import("estree").Node} node
|
|
825
|
+
*/
|
|
826
|
+
function reportRacFocusVisibleRing(node) {
|
|
827
|
+
if (extractStrings(node).some(hasBareRingClass)) context.report({
|
|
828
|
+
node,
|
|
829
|
+
messageId: "localFocusRing"
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
let skipFile = false;
|
|
833
|
+
return {
|
|
834
|
+
Program() {
|
|
835
|
+
skipFile = isFocusRingUtils(context.filename);
|
|
836
|
+
},
|
|
837
|
+
Literal(node) {
|
|
838
|
+
if (skipFile) return;
|
|
839
|
+
if (typeof node.value === "string") reportFocusRingStrings(node, [node.value]);
|
|
840
|
+
},
|
|
841
|
+
TemplateLiteral(node) {
|
|
842
|
+
if (skipFile) return;
|
|
843
|
+
reportFocusRingStrings(node, extractStrings(node));
|
|
844
|
+
},
|
|
845
|
+
ConditionalExpression(node) {
|
|
846
|
+
if (skipFile) return;
|
|
847
|
+
if (isFocusVisibleIdentifier(node.test)) reportRacFocusVisibleRing(node);
|
|
848
|
+
},
|
|
849
|
+
LogicalExpression(node) {
|
|
850
|
+
if (skipFile) return;
|
|
851
|
+
if (isFocusVisibleIdentifier(node.left) || isFocusVisibleIdentifier(node.right)) reportRacFocusVisibleRing(node);
|
|
852
|
+
}
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
});
|
|
856
|
+
//#endregion
|
|
857
|
+
//#region ../oxlint-plugin/rules/no-primitive-colors.js
|
|
858
|
+
const RULE_NAME$1 = "no-primitive-colors";
|
|
859
|
+
const LITERAL_RULE = "color-literal";
|
|
860
|
+
const ROLE_TOKENS = /* @__PURE__ */ new Set([
|
|
861
|
+
"background",
|
|
862
|
+
"foreground",
|
|
863
|
+
"card",
|
|
864
|
+
"card-foreground",
|
|
865
|
+
"card-soft",
|
|
866
|
+
"card-soft-foreground",
|
|
867
|
+
"popover",
|
|
868
|
+
"popover-foreground",
|
|
869
|
+
"muted",
|
|
870
|
+
"muted-foreground",
|
|
871
|
+
"accent",
|
|
872
|
+
"accent-foreground",
|
|
873
|
+
"feature",
|
|
874
|
+
"feature-bright",
|
|
875
|
+
"feature-foreground",
|
|
876
|
+
"primary",
|
|
877
|
+
"primary-foreground",
|
|
878
|
+
"primary-soft",
|
|
879
|
+
"primary-soft-foreground",
|
|
880
|
+
"secondary",
|
|
881
|
+
"secondary-foreground",
|
|
882
|
+
"secondary-soft",
|
|
883
|
+
"secondary-soft-foreground",
|
|
884
|
+
"brand",
|
|
885
|
+
"brand-foreground",
|
|
886
|
+
"error",
|
|
887
|
+
"error-foreground",
|
|
888
|
+
"error-soft",
|
|
889
|
+
"error-soft-foreground",
|
|
890
|
+
"info",
|
|
891
|
+
"info-foreground",
|
|
892
|
+
"info-soft",
|
|
893
|
+
"info-soft-foreground",
|
|
894
|
+
"success",
|
|
895
|
+
"success-foreground",
|
|
896
|
+
"success-soft",
|
|
897
|
+
"success-soft-foreground",
|
|
898
|
+
"warning",
|
|
899
|
+
"warning-foreground",
|
|
900
|
+
"warning-soft",
|
|
901
|
+
"warning-soft-foreground",
|
|
902
|
+
"border",
|
|
903
|
+
"input",
|
|
904
|
+
"ring",
|
|
905
|
+
"sidebar",
|
|
906
|
+
"sidebar-foreground",
|
|
907
|
+
"sidebar-accent",
|
|
908
|
+
"sidebar-accent-foreground",
|
|
909
|
+
"sidebar-border",
|
|
910
|
+
"sidebar-ring",
|
|
911
|
+
"sidebar-brand",
|
|
912
|
+
"sidebar-brand-foreground",
|
|
913
|
+
"right-panel",
|
|
914
|
+
"right-panel-foreground",
|
|
915
|
+
"chart-1",
|
|
916
|
+
"chart-2",
|
|
917
|
+
"chart-3",
|
|
918
|
+
"chart-4",
|
|
919
|
+
"chart-5",
|
|
920
|
+
"chart-6",
|
|
921
|
+
"chart-7",
|
|
922
|
+
"chart-8",
|
|
923
|
+
"sh-identifier",
|
|
924
|
+
"sh-keyword",
|
|
925
|
+
"sh-string",
|
|
926
|
+
"sh-class",
|
|
927
|
+
"sh-property",
|
|
928
|
+
"sh-entity",
|
|
929
|
+
"sh-jsxliterals",
|
|
930
|
+
"sh-sign",
|
|
931
|
+
"sh-comment"
|
|
932
|
+
]);
|
|
933
|
+
const TAILWIND_COLOR_FAMILIES = /* @__PURE__ */ new Set([
|
|
934
|
+
"red",
|
|
935
|
+
"orange",
|
|
936
|
+
"amber",
|
|
937
|
+
"yellow",
|
|
938
|
+
"lime",
|
|
939
|
+
"green",
|
|
940
|
+
"emerald",
|
|
941
|
+
"teal",
|
|
942
|
+
"cyan",
|
|
943
|
+
"sky",
|
|
944
|
+
"blue",
|
|
945
|
+
"indigo",
|
|
946
|
+
"violet",
|
|
947
|
+
"purple",
|
|
948
|
+
"fuchsia",
|
|
949
|
+
"pink",
|
|
950
|
+
"slate",
|
|
951
|
+
"gray",
|
|
952
|
+
"zinc",
|
|
953
|
+
"neutral",
|
|
954
|
+
"stone",
|
|
955
|
+
"black",
|
|
956
|
+
"white"
|
|
957
|
+
]);
|
|
958
|
+
const NON_COLOR_UTILITIES = /* @__PURE__ */ new Set([
|
|
959
|
+
"xs",
|
|
960
|
+
"sm",
|
|
961
|
+
"base",
|
|
962
|
+
"lg",
|
|
963
|
+
"xl",
|
|
964
|
+
"2xl",
|
|
965
|
+
"3xl",
|
|
966
|
+
"4xl",
|
|
967
|
+
"left",
|
|
968
|
+
"center",
|
|
969
|
+
"right",
|
|
970
|
+
"justify",
|
|
971
|
+
"wrap",
|
|
972
|
+
"nowrap",
|
|
973
|
+
"balance",
|
|
974
|
+
"pretty",
|
|
975
|
+
"ellipsis",
|
|
976
|
+
"clip",
|
|
977
|
+
"transparent",
|
|
978
|
+
"current",
|
|
979
|
+
"inherit",
|
|
980
|
+
"none",
|
|
981
|
+
"auto",
|
|
982
|
+
"color",
|
|
983
|
+
"0",
|
|
984
|
+
"2",
|
|
985
|
+
"4",
|
|
986
|
+
"8",
|
|
987
|
+
"t",
|
|
988
|
+
"r",
|
|
989
|
+
"b",
|
|
990
|
+
"l",
|
|
991
|
+
"x",
|
|
992
|
+
"y",
|
|
993
|
+
"solid",
|
|
994
|
+
"dashed",
|
|
995
|
+
"dotted",
|
|
996
|
+
"double",
|
|
997
|
+
"hidden",
|
|
998
|
+
"collapse",
|
|
999
|
+
"separate",
|
|
1000
|
+
"1",
|
|
1001
|
+
"inset",
|
|
1002
|
+
"inner"
|
|
1003
|
+
]);
|
|
1004
|
+
const NON_COLOR_PATTERNS = [
|
|
1005
|
+
/^linear-to-[trbl]{1,2}$/,
|
|
1006
|
+
/^[trblxy]-\d+$/,
|
|
1007
|
+
/^offset-\d+$/,
|
|
1008
|
+
/^\d+$/,
|
|
1009
|
+
/^clip-.+$/
|
|
1010
|
+
];
|
|
1011
|
+
const TOKEN_RE = /(?:^|[^a-zA-Z0-9-])(((?:[a-z-]+:)*)?(?:bg|border|text|ring(?:-offset)?|fill|stroke|placeholder|caret|accent|decoration|divide|outline|from|via|to)-([a-z][a-z0-9-]*)(?:-\d{2,3})?(?:\/[0-9]{1,3})?)/gim;
|
|
1012
|
+
const ARBITRARY_COLOR_RE = /\[(?:#[0-9a-fA-F]{3,8}(?:\/[\d.]+)?|(?:oklch|oklab|lab|lch|rgb|rgba|hsl|hsla|hwb)\()/i;
|
|
1013
|
+
const ALLOWED_EXACT_CLASSES = [
|
|
1014
|
+
"bg-black/10",
|
|
1015
|
+
"outline-black/10",
|
|
1016
|
+
"bg-[repeating-linear-gradient(45deg,transparent,transparent_8px,rgb(0_0_0/0.02)_8px,rgb(0_0_0/0.02)_16px)]"
|
|
1017
|
+
];
|
|
1018
|
+
/**
|
|
1019
|
+
* @param {string} tokenName
|
|
1020
|
+
*/
|
|
1021
|
+
function isNonColorUtility(tokenName) {
|
|
1022
|
+
if (NON_COLOR_UTILITIES.has(tokenName)) return true;
|
|
1023
|
+
return NON_COLOR_PATTERNS.some((pattern) => pattern.test(tokenName));
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* @param {string} token
|
|
1027
|
+
*/
|
|
1028
|
+
function isAllowedExactClass(token) {
|
|
1029
|
+
return ALLOWED_EXACT_CLASSES.includes(token);
|
|
1030
|
+
}
|
|
1031
|
+
/**
|
|
1032
|
+
* Whole class tokens only. Variant prefixes are part of the
|
|
1033
|
+
* token, so `hover:bg-black/10` is not the documented `bg-black/10` literal.
|
|
1034
|
+
*
|
|
1035
|
+
* @param {string} str
|
|
1036
|
+
*/
|
|
1037
|
+
function stripAllowedClasses(str) {
|
|
1038
|
+
return str.split(/\s+/).filter((token) => token.length > 0 && !isAllowedExactClass(token)).join(" ");
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* @param {string} str
|
|
1042
|
+
*/
|
|
1043
|
+
function findPrimitiveColor(str) {
|
|
1044
|
+
TOKEN_RE.lastIndex = 0;
|
|
1045
|
+
let match;
|
|
1046
|
+
while (match = TOKEN_RE.exec(str)) {
|
|
1047
|
+
const fullToken = match[1];
|
|
1048
|
+
const colorFamily = match[3];
|
|
1049
|
+
if (!fullToken || !colorFamily) continue;
|
|
1050
|
+
const tokenName = colorFamily.replace(/\/\d+$/, "");
|
|
1051
|
+
if (isNonColorUtility(tokenName)) continue;
|
|
1052
|
+
if (ROLE_TOKENS.has(tokenName)) continue;
|
|
1053
|
+
const primitiveFamily = tokenName.replace(/-\d+$/, "");
|
|
1054
|
+
if (TAILWIND_COLOR_FAMILIES.has(primitiveFamily) || TAILWIND_COLOR_FAMILIES.has(tokenName)) return fullToken;
|
|
1055
|
+
}
|
|
1056
|
+
return null;
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* @param {string} str
|
|
1060
|
+
*/
|
|
1061
|
+
function hasForbiddenColorLiteral(str) {
|
|
1062
|
+
return ARBITRARY_COLOR_RE.test(str);
|
|
1063
|
+
}
|
|
1064
|
+
var no_primitive_colors_default = defineRule({
|
|
1065
|
+
meta: {
|
|
1066
|
+
type: "problem",
|
|
1067
|
+
docs: { description: "Disallow raw palette classes and color literals; use role tokens" },
|
|
1068
|
+
messages: {
|
|
1069
|
+
[RULE_NAME$1]: "Avoid raw palette classes (e.g. `bg-white`, `text-slate-500`). Style with role tokens only.",
|
|
1070
|
+
[LITERAL_RULE]: "Avoid hex/oklch/rgb color literals in class strings. Style with role tokens only."
|
|
1071
|
+
},
|
|
1072
|
+
schema: []
|
|
1073
|
+
},
|
|
1074
|
+
defaultOptions: [],
|
|
1075
|
+
createOnce(context) {
|
|
1076
|
+
/**
|
|
1077
|
+
* @param {import("estree").Node} node
|
|
1078
|
+
* @param {string[]} collected
|
|
1079
|
+
*/
|
|
1080
|
+
function reportColorIssues(node, collected) {
|
|
1081
|
+
for (const raw of collected) {
|
|
1082
|
+
const value = stripAllowedClasses(raw);
|
|
1083
|
+
if (findPrimitiveColor(value)) {
|
|
1084
|
+
context.report({
|
|
1085
|
+
node,
|
|
1086
|
+
messageId: RULE_NAME$1
|
|
1087
|
+
});
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
if (hasForbiddenColorLiteral(value)) {
|
|
1091
|
+
context.report({
|
|
1092
|
+
node,
|
|
1093
|
+
messageId: LITERAL_RULE
|
|
1094
|
+
});
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
return {
|
|
1100
|
+
JSXAttribute(node) {
|
|
1101
|
+
const name = node.name.type === "JSXIdentifier" ? node.name.name : void 0;
|
|
1102
|
+
if (name !== "className" && name !== "class") return;
|
|
1103
|
+
if (node.value) reportColorIssues(node, extractStrings(node.value));
|
|
1104
|
+
},
|
|
1105
|
+
CallExpression(node) {
|
|
1106
|
+
if (!isNamedCall(node.callee, "tv") && !isNamedCall(node.callee, "cn")) return;
|
|
1107
|
+
reportColorIssues(node, extractStrings(node));
|
|
1108
|
+
},
|
|
1109
|
+
Literal(node) {
|
|
1110
|
+
if (typeof node.value === "string") reportColorIssues(node, [node.value]);
|
|
1111
|
+
},
|
|
1112
|
+
TemplateLiteral(node) {
|
|
1113
|
+
reportColorIssues(node, extractStrings(node));
|
|
1114
|
+
}
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
});
|
|
1118
|
+
//#endregion
|
|
1119
|
+
//#region ../oxlint-plugin/forbidden-rac-packages.js
|
|
1120
|
+
/**
|
|
1121
|
+
* React Aria packages restricted to the quarantine directory, including
|
|
1122
|
+
* their `@react-aria/*` and `@react-stately/*` dependencies.
|
|
1123
|
+
*
|
|
1124
|
+
* Package-owned policy used by `elmera/no-rac-outside-quarantine`.
|
|
1125
|
+
*
|
|
1126
|
+
* @type {readonly string[]}
|
|
1127
|
+
*/
|
|
1128
|
+
const FORBIDDEN_RAC_PACKAGES = Object.freeze([
|
|
1129
|
+
"react-aria-components",
|
|
1130
|
+
"react-aria",
|
|
1131
|
+
"@internationalized/date",
|
|
1132
|
+
"@react-aria",
|
|
1133
|
+
"@react-stately"
|
|
1134
|
+
]);
|
|
1135
|
+
/**
|
|
1136
|
+
* @param {string} specifier
|
|
1137
|
+
* @returns {boolean}
|
|
1138
|
+
*/
|
|
1139
|
+
function isForbiddenRacSpecifier(specifier) {
|
|
1140
|
+
for (const name of FORBIDDEN_RAC_PACKAGES) if (specifier === name || specifier.startsWith(`${name}/`)) return true;
|
|
1141
|
+
return false;
|
|
1142
|
+
}
|
|
1143
|
+
//#endregion
|
|
1144
|
+
//#region ../oxlint-plugin/rules/no-rac-outside-quarantine.js
|
|
1145
|
+
/**
|
|
1146
|
+
* @param {string} filename
|
|
1147
|
+
*/
|
|
1148
|
+
function isReactAriaQuarantine(filename) {
|
|
1149
|
+
return normalizeFilename(filename).includes("/src/react-aria/");
|
|
1150
|
+
}
|
|
1151
|
+
/**
|
|
1152
|
+
* @param {import("estree").Node | null | undefined} source
|
|
1153
|
+
* @returns {string | null}
|
|
1154
|
+
*/
|
|
1155
|
+
function specifierFromSource(source) {
|
|
1156
|
+
if (source?.type === "Literal" && typeof source.value === "string") return source.value;
|
|
1157
|
+
return null;
|
|
1158
|
+
}
|
|
1159
|
+
var no_rac_outside_quarantine_default = defineRule({
|
|
1160
|
+
meta: {
|
|
1161
|
+
type: "problem",
|
|
1162
|
+
docs: { description: "Forbid react-aria-components, react-aria, @internationalized/date, and the scoped @react-aria/* / @react-stately/* packages outside src/react-aria/**" },
|
|
1163
|
+
messages: { quarantined: "`{{specifier}}` may only be imported from packages/ui/src/react-aria/** (quarantine)." },
|
|
1164
|
+
schema: []
|
|
1165
|
+
},
|
|
1166
|
+
defaultOptions: [],
|
|
1167
|
+
createOnce(context) {
|
|
1168
|
+
let skipFile = false;
|
|
1169
|
+
/**
|
|
1170
|
+
* @param {import("estree").Node} node
|
|
1171
|
+
* @param {string | null} specifier
|
|
1172
|
+
*/
|
|
1173
|
+
function reportIfForbidden(node, specifier) {
|
|
1174
|
+
if (skipFile || specifier === null || !isForbiddenRacSpecifier(specifier)) return;
|
|
1175
|
+
context.report({
|
|
1176
|
+
node,
|
|
1177
|
+
messageId: "quarantined",
|
|
1178
|
+
data: { specifier }
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
return {
|
|
1182
|
+
Program() {
|
|
1183
|
+
skipFile = isReactAriaQuarantine(context.filename);
|
|
1184
|
+
},
|
|
1185
|
+
ImportDeclaration(node) {
|
|
1186
|
+
reportIfForbidden(node, specifierFromSource(node.source));
|
|
1187
|
+
},
|
|
1188
|
+
ExportNamedDeclaration(node) {
|
|
1189
|
+
reportIfForbidden(node, specifierFromSource(node.source));
|
|
1190
|
+
},
|
|
1191
|
+
ExportAllDeclaration(node) {
|
|
1192
|
+
reportIfForbidden(node, specifierFromSource(node.source));
|
|
1193
|
+
},
|
|
1194
|
+
ImportExpression(node) {
|
|
1195
|
+
reportIfForbidden(node, specifierFromSource(node.source));
|
|
1196
|
+
}
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
});
|
|
1200
|
+
//#endregion
|
|
1201
|
+
//#region ../oxlint-plugin/rules/no-raw-class-map.js
|
|
1202
|
+
/**
|
|
1203
|
+
* Exact utilities that are legal class tokens without a hyphenated suffix.
|
|
1204
|
+
* `z-*` is omitted on purpose: `overlayLayer = "z-50"` is the one-owner keep.
|
|
1205
|
+
*/
|
|
1206
|
+
const EXACT_UTILITIES = /* @__PURE__ */ new Set([
|
|
1207
|
+
"flex",
|
|
1208
|
+
"grid",
|
|
1209
|
+
"block",
|
|
1210
|
+
"inline",
|
|
1211
|
+
"inline-block",
|
|
1212
|
+
"inline-flex",
|
|
1213
|
+
"inline-grid",
|
|
1214
|
+
"inline-table",
|
|
1215
|
+
"contents",
|
|
1216
|
+
"hidden",
|
|
1217
|
+
"visible",
|
|
1218
|
+
"invisible",
|
|
1219
|
+
"collapse",
|
|
1220
|
+
"isolate",
|
|
1221
|
+
"relative",
|
|
1222
|
+
"absolute",
|
|
1223
|
+
"sticky",
|
|
1224
|
+
"fixed",
|
|
1225
|
+
"static",
|
|
1226
|
+
"grow",
|
|
1227
|
+
"shrink",
|
|
1228
|
+
"truncate",
|
|
1229
|
+
"underline",
|
|
1230
|
+
"overline",
|
|
1231
|
+
"line-through",
|
|
1232
|
+
"no-underline",
|
|
1233
|
+
"italic",
|
|
1234
|
+
"not-italic",
|
|
1235
|
+
"uppercase",
|
|
1236
|
+
"lowercase",
|
|
1237
|
+
"capitalize",
|
|
1238
|
+
"normal-case",
|
|
1239
|
+
"border",
|
|
1240
|
+
"rounded",
|
|
1241
|
+
"shadow",
|
|
1242
|
+
"ring",
|
|
1243
|
+
"outline",
|
|
1244
|
+
"blur",
|
|
1245
|
+
"group",
|
|
1246
|
+
"peer",
|
|
1247
|
+
"container",
|
|
1248
|
+
"table",
|
|
1249
|
+
"table-cell",
|
|
1250
|
+
"table-row",
|
|
1251
|
+
"table-column",
|
|
1252
|
+
"list-item",
|
|
1253
|
+
"flow-root",
|
|
1254
|
+
"antialiased",
|
|
1255
|
+
"subpixel-antialiased",
|
|
1256
|
+
"sr-only",
|
|
1257
|
+
"not-sr-only",
|
|
1258
|
+
"tabular-nums",
|
|
1259
|
+
"select-none",
|
|
1260
|
+
"select-text",
|
|
1261
|
+
"select-all",
|
|
1262
|
+
"select-auto",
|
|
1263
|
+
"pointer-events-none",
|
|
1264
|
+
"pointer-events-auto",
|
|
1265
|
+
"appearance-none",
|
|
1266
|
+
"resize",
|
|
1267
|
+
"resize-none",
|
|
1268
|
+
"resize-x",
|
|
1269
|
+
"resize-y",
|
|
1270
|
+
"not-prose",
|
|
1271
|
+
"prose"
|
|
1272
|
+
]);
|
|
1273
|
+
/**
|
|
1274
|
+
* Hyphenated (or bracket/paren) Tailwind prefixes. Single-letter prefixes keep the
|
|
1275
|
+
* hyphen so `p-` does not match `px-`/`previous`.
|
|
1276
|
+
*/
|
|
1277
|
+
const UTILITY_PREFIXES = [
|
|
1278
|
+
"text-",
|
|
1279
|
+
"bg-",
|
|
1280
|
+
"border-",
|
|
1281
|
+
"flex-",
|
|
1282
|
+
"grid-",
|
|
1283
|
+
"gap-",
|
|
1284
|
+
"p-",
|
|
1285
|
+
"px-",
|
|
1286
|
+
"py-",
|
|
1287
|
+
"pt-",
|
|
1288
|
+
"pr-",
|
|
1289
|
+
"pb-",
|
|
1290
|
+
"pl-",
|
|
1291
|
+
"ps-",
|
|
1292
|
+
"pe-",
|
|
1293
|
+
"m-",
|
|
1294
|
+
"mx-",
|
|
1295
|
+
"my-",
|
|
1296
|
+
"mt-",
|
|
1297
|
+
"mr-",
|
|
1298
|
+
"mb-",
|
|
1299
|
+
"ml-",
|
|
1300
|
+
"ms-",
|
|
1301
|
+
"me-",
|
|
1302
|
+
"size-",
|
|
1303
|
+
"h-",
|
|
1304
|
+
"w-",
|
|
1305
|
+
"min-h-",
|
|
1306
|
+
"min-w-",
|
|
1307
|
+
"max-h-",
|
|
1308
|
+
"max-w-",
|
|
1309
|
+
"rounded-",
|
|
1310
|
+
"font-",
|
|
1311
|
+
"items-",
|
|
1312
|
+
"justify-",
|
|
1313
|
+
"self-",
|
|
1314
|
+
"content-",
|
|
1315
|
+
"place-",
|
|
1316
|
+
"shadow-",
|
|
1317
|
+
"ring-",
|
|
1318
|
+
"outline-",
|
|
1319
|
+
"opacity-",
|
|
1320
|
+
"scale-",
|
|
1321
|
+
"blur-",
|
|
1322
|
+
"rotate-",
|
|
1323
|
+
"translate-",
|
|
1324
|
+
"skew-",
|
|
1325
|
+
"origin-",
|
|
1326
|
+
"duration-",
|
|
1327
|
+
"ease-",
|
|
1328
|
+
"delay-",
|
|
1329
|
+
"transition-",
|
|
1330
|
+
"animate-",
|
|
1331
|
+
"slide-",
|
|
1332
|
+
"fade-",
|
|
1333
|
+
"zoom-",
|
|
1334
|
+
"spin-",
|
|
1335
|
+
"cursor-",
|
|
1336
|
+
"pointer-events-",
|
|
1337
|
+
"select-",
|
|
1338
|
+
"overflow-",
|
|
1339
|
+
"overscroll-",
|
|
1340
|
+
"whitespace-",
|
|
1341
|
+
"break-",
|
|
1342
|
+
"leading-",
|
|
1343
|
+
"tracking-",
|
|
1344
|
+
"indent-",
|
|
1345
|
+
"align-",
|
|
1346
|
+
"list-",
|
|
1347
|
+
"decoration-",
|
|
1348
|
+
"underline-offset-",
|
|
1349
|
+
"line-clamp-",
|
|
1350
|
+
"from-",
|
|
1351
|
+
"via-",
|
|
1352
|
+
"to-",
|
|
1353
|
+
"divide-",
|
|
1354
|
+
"space-",
|
|
1355
|
+
"basis-",
|
|
1356
|
+
"grow-",
|
|
1357
|
+
"shrink-",
|
|
1358
|
+
"col-",
|
|
1359
|
+
"row-",
|
|
1360
|
+
"order-",
|
|
1361
|
+
"inset-",
|
|
1362
|
+
"top-",
|
|
1363
|
+
"right-",
|
|
1364
|
+
"bottom-",
|
|
1365
|
+
"left-",
|
|
1366
|
+
"start-",
|
|
1367
|
+
"end-",
|
|
1368
|
+
"object-",
|
|
1369
|
+
"aspect-",
|
|
1370
|
+
"fill-",
|
|
1371
|
+
"stroke-",
|
|
1372
|
+
"accent-",
|
|
1373
|
+
"caret-",
|
|
1374
|
+
"scroll-",
|
|
1375
|
+
"snap-",
|
|
1376
|
+
"touch-",
|
|
1377
|
+
"will-change-",
|
|
1378
|
+
"backdrop-",
|
|
1379
|
+
"drop-shadow-",
|
|
1380
|
+
"brightness-",
|
|
1381
|
+
"contrast-",
|
|
1382
|
+
"saturate-",
|
|
1383
|
+
"hue-rotate-",
|
|
1384
|
+
"grayscale-",
|
|
1385
|
+
"invert-",
|
|
1386
|
+
"sepia-",
|
|
1387
|
+
"mix-blend-",
|
|
1388
|
+
"filter-",
|
|
1389
|
+
"transform-",
|
|
1390
|
+
"caption-",
|
|
1391
|
+
"hyphens-",
|
|
1392
|
+
"columns-",
|
|
1393
|
+
"float-",
|
|
1394
|
+
"clear-",
|
|
1395
|
+
"box-",
|
|
1396
|
+
"isolation-",
|
|
1397
|
+
"hit-area-",
|
|
1398
|
+
"auto-rows-",
|
|
1399
|
+
"auto-cols-",
|
|
1400
|
+
"@container"
|
|
1401
|
+
];
|
|
1402
|
+
/**
|
|
1403
|
+
* @param {string} filename
|
|
1404
|
+
*/
|
|
1405
|
+
function isTestFile$1(filename) {
|
|
1406
|
+
return filename.endsWith(".test.ts") || filename.endsWith(".test.tsx") || filename.endsWith(".browser.test.tsx") || filename.endsWith(".test-d.tsx");
|
|
1407
|
+
}
|
|
1408
|
+
/**
|
|
1409
|
+
* @param {string} filename
|
|
1410
|
+
*/
|
|
1411
|
+
function isSkippedPath(filename) {
|
|
1412
|
+
const normalized = normalizeFilename(filename);
|
|
1413
|
+
if (isTestFile$1(normalized)) return true;
|
|
1414
|
+
if (normalized.includes("/intl/")) return true;
|
|
1415
|
+
if (normalized.includes("/generated/")) return true;
|
|
1416
|
+
return false;
|
|
1417
|
+
}
|
|
1418
|
+
/**
|
|
1419
|
+
* @param {import("estree").Node | null | undefined} node
|
|
1420
|
+
*/
|
|
1421
|
+
function unwrap(node) {
|
|
1422
|
+
let current = node;
|
|
1423
|
+
while (current) {
|
|
1424
|
+
if (current.type === "TSAsExpression" || current.type === "TSSatisfiesExpression" || current.type === "TSTypeAssertion" || current.type === "ParenthesizedExpression" || current.type === "ChainExpression") {
|
|
1425
|
+
current = current.expression;
|
|
1426
|
+
continue;
|
|
1427
|
+
}
|
|
1428
|
+
break;
|
|
1429
|
+
}
|
|
1430
|
+
return current;
|
|
1431
|
+
}
|
|
1432
|
+
/**
|
|
1433
|
+
* @param {string} str
|
|
1434
|
+
*/
|
|
1435
|
+
function classTokens(str) {
|
|
1436
|
+
return str.split(/\s+/).filter(Boolean);
|
|
1437
|
+
}
|
|
1438
|
+
/**
|
|
1439
|
+
* Last `:` outside brackets, then strip important/negative modifiers.
|
|
1440
|
+
* @param {string} token
|
|
1441
|
+
*/
|
|
1442
|
+
function utilityOf(token) {
|
|
1443
|
+
let depth = 0;
|
|
1444
|
+
let lastColon = -1;
|
|
1445
|
+
for (let i = 0; i < token.length; i += 1) {
|
|
1446
|
+
const ch = token[i];
|
|
1447
|
+
if (ch === "[") depth += 1;
|
|
1448
|
+
else if (ch === "]") depth = Math.max(0, depth - 1);
|
|
1449
|
+
else if (ch === ":" && depth === 0) lastColon = i;
|
|
1450
|
+
}
|
|
1451
|
+
let utility = lastColon === -1 ? token : token.slice(lastColon + 1);
|
|
1452
|
+
if (utility.startsWith("!")) utility = utility.slice(1);
|
|
1453
|
+
if (utility.endsWith("!")) utility = utility.slice(0, -1);
|
|
1454
|
+
if (utility.startsWith("-") && utility.length > 1 && /[a-z@]/i.test(utility[1])) utility = utility.slice(1);
|
|
1455
|
+
return utility;
|
|
1456
|
+
}
|
|
1457
|
+
/**
|
|
1458
|
+
* @param {string} token
|
|
1459
|
+
*/
|
|
1460
|
+
function isArbitraryProperty(token) {
|
|
1461
|
+
return token.startsWith("[") && token.endsWith("]") && (token.startsWith("[--") || token.includes(":"));
|
|
1462
|
+
}
|
|
1463
|
+
/**
|
|
1464
|
+
* @param {string} token
|
|
1465
|
+
*/
|
|
1466
|
+
function isPrefixedOrArbitraryUtility(token) {
|
|
1467
|
+
if (!token) return false;
|
|
1468
|
+
if (isArbitraryProperty(token)) return true;
|
|
1469
|
+
if (token.startsWith("group/") || token.startsWith("peer/")) return true;
|
|
1470
|
+
return UTILITY_PREFIXES.some((prefix) => token.startsWith(prefix));
|
|
1471
|
+
}
|
|
1472
|
+
/**
|
|
1473
|
+
* @param {string} token
|
|
1474
|
+
*/
|
|
1475
|
+
function isTailwindUtility(token) {
|
|
1476
|
+
if (!token) return false;
|
|
1477
|
+
if (isPrefixedOrArbitraryUtility(token)) return true;
|
|
1478
|
+
return EXACT_UTILITIES.has(token);
|
|
1479
|
+
}
|
|
1480
|
+
/**
|
|
1481
|
+
* @param {string} raw
|
|
1482
|
+
*/
|
|
1483
|
+
function isTailwindToken(raw) {
|
|
1484
|
+
return isTailwindUtility(utilityOf(raw));
|
|
1485
|
+
}
|
|
1486
|
+
/**
|
|
1487
|
+
* A string looks like a class list when it has a hyphenated/arbitrary utility, or when
|
|
1488
|
+
* every token is a utility (`"flex isolate"`). Exact-only hits such as the English word
|
|
1489
|
+
* `block` in a comment are not enough.
|
|
1490
|
+
*
|
|
1491
|
+
* @param {string} str
|
|
1492
|
+
*/
|
|
1493
|
+
function looksLikeClassString(str) {
|
|
1494
|
+
const tokens = classTokens(str);
|
|
1495
|
+
if (tokens.length === 0) return false;
|
|
1496
|
+
const hits = tokens.filter(isTailwindToken);
|
|
1497
|
+
if (hits.length === 0) return false;
|
|
1498
|
+
if (hits.some((token) => isPrefixedOrArbitraryUtility(utilityOf(token)))) return true;
|
|
1499
|
+
return hits.length === tokens.length;
|
|
1500
|
+
}
|
|
1501
|
+
/**
|
|
1502
|
+
* @param {string} name
|
|
1503
|
+
*/
|
|
1504
|
+
function looksLikeClassIdentifier(name) {
|
|
1505
|
+
return /(?:Class(?:es|Name)?|classNames)$/.test(name);
|
|
1506
|
+
}
|
|
1507
|
+
/**
|
|
1508
|
+
* @param {import("estree").Node | null | undefined} node
|
|
1509
|
+
* @param {string[]} out
|
|
1510
|
+
*/
|
|
1511
|
+
function collectValueStrings(node, out) {
|
|
1512
|
+
const current = unwrap(node);
|
|
1513
|
+
if (!current) return;
|
|
1514
|
+
switch (current.type) {
|
|
1515
|
+
case "Literal":
|
|
1516
|
+
if (typeof current.value === "string") out.push(current.value);
|
|
1517
|
+
break;
|
|
1518
|
+
case "TemplateLiteral":
|
|
1519
|
+
out.push(...extractStrings(current));
|
|
1520
|
+
break;
|
|
1521
|
+
case "ObjectExpression":
|
|
1522
|
+
for (const prop of current.properties) if (prop.type === "Property") collectValueStrings(prop.value, out);
|
|
1523
|
+
break;
|
|
1524
|
+
case "ArrayExpression": for (const el of current.elements) if (el) collectValueStrings(el, out);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
/**
|
|
1528
|
+
* @param {import("estree").ObjectExpression} node
|
|
1529
|
+
*/
|
|
1530
|
+
function objectLooksLikeClassMap(node) {
|
|
1531
|
+
/** @type {string[]} */
|
|
1532
|
+
const values = [];
|
|
1533
|
+
collectValueStrings(node, values);
|
|
1534
|
+
return values.some(looksLikeClassString);
|
|
1535
|
+
}
|
|
1536
|
+
/**
|
|
1537
|
+
* @param {import("estree").TemplateLiteral} node
|
|
1538
|
+
*/
|
|
1539
|
+
function templateLooksLikeClassString(node) {
|
|
1540
|
+
if (looksLikeClassString(node.quasis.map((quasi) => quasi.value.cooked ?? "").join(" "))) return true;
|
|
1541
|
+
if (node.expressions.length === 0) return false;
|
|
1542
|
+
if (!node.expressions.every((expr) => expr.type === "Identifier")) return false;
|
|
1543
|
+
return node.expressions.some((expr) => expr.type === "Identifier" && looksLikeClassIdentifier(expr.name));
|
|
1544
|
+
}
|
|
1545
|
+
var no_raw_class_map_default = defineRule({
|
|
1546
|
+
meta: {
|
|
1547
|
+
type: "problem",
|
|
1548
|
+
docs: { description: "Forbid raw Tailwind class maps and hand-spelled class constants. Maps with an axis or two-plus slots are tv() recipes; a single axis-less string is cn(). Any call-expression initializer is accepted. EXACT_UTILITIES and UTILITY_PREFIXES are the heuristic boundary for 'looks like Tailwind'. Skips tests, *.test-d.tsx, intl dictionaries, and generated files" },
|
|
1549
|
+
messages: { rawClassMap: "Class maps with an axis or two or more slots are `tv()` recipes; a single axis-less class string is `cn(\"…\")`. Resolved string constants are legal when they are `cn(…)` or a recipe call, never spelled by hand." },
|
|
1550
|
+
schema: []
|
|
1551
|
+
},
|
|
1552
|
+
defaultOptions: [],
|
|
1553
|
+
createOnce(context) {
|
|
1554
|
+
let skipFile = false;
|
|
1555
|
+
/**
|
|
1556
|
+
* @param {import("estree").Node} node
|
|
1557
|
+
*/
|
|
1558
|
+
function report(node) {
|
|
1559
|
+
context.report({
|
|
1560
|
+
node,
|
|
1561
|
+
messageId: "rawClassMap"
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
return {
|
|
1565
|
+
Program() {
|
|
1566
|
+
skipFile = isSkippedPath(context.filename);
|
|
1567
|
+
},
|
|
1568
|
+
VariableDeclarator(node) {
|
|
1569
|
+
if (skipFile || !node.init) return;
|
|
1570
|
+
const init = unwrap(node.init);
|
|
1571
|
+
if (!init) return;
|
|
1572
|
+
if (init.type === "CallExpression") return;
|
|
1573
|
+
if (init.type === "ObjectExpression") {
|
|
1574
|
+
if (objectLooksLikeClassMap(init)) report(init);
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1577
|
+
if (init.type === "Literal" && typeof init.value === "string") {
|
|
1578
|
+
if (looksLikeClassString(init.value)) report(init);
|
|
1579
|
+
return;
|
|
1580
|
+
}
|
|
1581
|
+
if (init.type === "TemplateLiteral" && templateLooksLikeClassString(init)) report(init);
|
|
1582
|
+
}
|
|
1583
|
+
};
|
|
1584
|
+
}
|
|
1585
|
+
});
|
|
1586
|
+
//#endregion
|
|
1587
|
+
//#region ../oxlint-plugin/rules/no-tailwind-dark-variant.js
|
|
1588
|
+
const RULE_NAME = "no-tailwind-dark-variant";
|
|
1589
|
+
/**
|
|
1590
|
+
* @param {string} str
|
|
1591
|
+
*/
|
|
1592
|
+
function hasDarkVariant(str) {
|
|
1593
|
+
return /\bdark:[a-z]+[-\w]*/.test(str);
|
|
1594
|
+
}
|
|
1595
|
+
/**
|
|
1596
|
+
* @param {import("estree").Node} node
|
|
1597
|
+
*/
|
|
1598
|
+
function isInsideJsxAttribute(node) {
|
|
1599
|
+
let current = node.parent;
|
|
1600
|
+
while (current) {
|
|
1601
|
+
if (current.type === "JSXAttribute") return true;
|
|
1602
|
+
current = current.parent;
|
|
1603
|
+
}
|
|
1604
|
+
return false;
|
|
1605
|
+
}
|
|
1606
|
+
var no_tailwind_dark_variant_default = defineRule({
|
|
1607
|
+
meta: {
|
|
1608
|
+
type: "problem",
|
|
1609
|
+
docs: { description: "Disallow Tailwind dark: variant usage in library source" },
|
|
1610
|
+
messages: { [RULE_NAME]: "Avoid Tailwind's dark: variant. The dark axis is token-reserved behind [data-theme=\"dark\"]." },
|
|
1611
|
+
schema: []
|
|
1612
|
+
},
|
|
1613
|
+
defaultOptions: [],
|
|
1614
|
+
createOnce(context) {
|
|
1615
|
+
/**
|
|
1616
|
+
* @param {import("estree").Node} node
|
|
1617
|
+
* @param {string[]} collected
|
|
1618
|
+
*/
|
|
1619
|
+
function reportIfDark(node, collected) {
|
|
1620
|
+
for (const s of collected) if (hasDarkVariant(s)) {
|
|
1621
|
+
context.report({
|
|
1622
|
+
node,
|
|
1623
|
+
messageId: RULE_NAME
|
|
1624
|
+
});
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
return {
|
|
1629
|
+
JSXAttribute(node) {
|
|
1630
|
+
const name = node.name.type === "JSXIdentifier" ? node.name.name : void 0;
|
|
1631
|
+
if (name !== "className" && name !== "class") return;
|
|
1632
|
+
if (node.value) reportIfDark(node, extractStrings(node.value));
|
|
1633
|
+
},
|
|
1634
|
+
Literal(node) {
|
|
1635
|
+
if (typeof node.value !== "string" || !hasDarkVariant(node.value) || isInsideJsxAttribute(node)) return;
|
|
1636
|
+
context.report({
|
|
1637
|
+
node,
|
|
1638
|
+
messageId: RULE_NAME
|
|
1639
|
+
});
|
|
1640
|
+
},
|
|
1641
|
+
TemplateLiteral(node) {
|
|
1642
|
+
if (isInsideJsxAttribute(node)) return;
|
|
1643
|
+
if (extractStrings(node).some(hasDarkVariant)) context.report({
|
|
1644
|
+
node,
|
|
1645
|
+
messageId: RULE_NAME
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1650
|
+
});
|
|
1651
|
+
//#endregion
|
|
1652
|
+
//#region ../oxlint-plugin/rules/require-icon-button-label.js
|
|
1653
|
+
const ICON_SIZE_PREFIX = "icon";
|
|
1654
|
+
const TEXT_CONTENT_NAMES = /* @__PURE__ */ new Set([
|
|
1655
|
+
"Span",
|
|
1656
|
+
"Text",
|
|
1657
|
+
"ItemTitle",
|
|
1658
|
+
"Title"
|
|
1659
|
+
]);
|
|
1660
|
+
/**
|
|
1661
|
+
* Statically known string from a JSX attribute value or child expression.
|
|
1662
|
+
* Identifiers and interpolated templates stay unknown.
|
|
1663
|
+
*
|
|
1664
|
+
* @param {import("estree").Node | null | undefined} node
|
|
1665
|
+
* @returns {string | null}
|
|
1666
|
+
*/
|
|
1667
|
+
function getStaticString(node) {
|
|
1668
|
+
if (!node) return null;
|
|
1669
|
+
if (node.type === "Literal") return typeof node.value === "string" ? node.value : null;
|
|
1670
|
+
if (node.type === "TemplateLiteral") {
|
|
1671
|
+
if (node.expressions.length !== 0) return null;
|
|
1672
|
+
const cooked = node.quasis[0]?.value.cooked;
|
|
1673
|
+
return typeof cooked === "string" ? cooked : null;
|
|
1674
|
+
}
|
|
1675
|
+
if (node.type === "JSXExpressionContainer") return getStaticString(node.expression);
|
|
1676
|
+
return null;
|
|
1677
|
+
}
|
|
1678
|
+
/**
|
|
1679
|
+
* Known nonempty strings count. Known empty/whitespace, bare, null, and false do not.
|
|
1680
|
+
* Unresolved expressions stay permissive.
|
|
1681
|
+
*
|
|
1682
|
+
* @param {import("estree").JSXOpeningElement} node
|
|
1683
|
+
*/
|
|
1684
|
+
function hasUsableAriaLabel(node) {
|
|
1685
|
+
return node.attributes.some((attr) => {
|
|
1686
|
+
if (attr.type !== "JSXAttribute" || attr.name.name !== "aria-label") return false;
|
|
1687
|
+
if (!attr.value) return false;
|
|
1688
|
+
const staticString = getStaticString(attr.value);
|
|
1689
|
+
if (staticString !== null) return staticString.trim().length > 0;
|
|
1690
|
+
if ((attr.value.type === "JSXExpressionContainer" ? attr.value.expression : attr.value).type === "Literal") return false;
|
|
1691
|
+
return true;
|
|
1692
|
+
});
|
|
1693
|
+
}
|
|
1694
|
+
function hasSlot(node) {
|
|
1695
|
+
return node.attributes.some((attr) => attr.type === "JSXAttribute" && attr.name.name === "slot");
|
|
1696
|
+
}
|
|
1697
|
+
/**
|
|
1698
|
+
* Size/variant may also use an identifier's name as a heuristic (size={icon}).
|
|
1699
|
+
* That heuristic is not a known runtime string.
|
|
1700
|
+
*
|
|
1701
|
+
* @param {import("estree").JSXAttribute} attr
|
|
1702
|
+
* @returns {string | null}
|
|
1703
|
+
*/
|
|
1704
|
+
function getSizeOrVariantValue(attr) {
|
|
1705
|
+
const staticValue = getStaticString(attr.value);
|
|
1706
|
+
if (staticValue !== null) return staticValue;
|
|
1707
|
+
if (attr.value?.type === "JSXExpressionContainer" && attr.value.expression.type === "Identifier") return attr.value.expression.name;
|
|
1708
|
+
return null;
|
|
1709
|
+
}
|
|
1710
|
+
function isIconVariant(node) {
|
|
1711
|
+
return node.attributes.some((attr) => {
|
|
1712
|
+
if (attr.type !== "JSXAttribute" || attr.name.name !== "variant") return false;
|
|
1713
|
+
return getSizeOrVariantValue(attr) === "icon";
|
|
1714
|
+
});
|
|
1715
|
+
}
|
|
1716
|
+
function isIconSize(node) {
|
|
1717
|
+
return node.attributes.some((attr) => {
|
|
1718
|
+
if (attr.type !== "JSXAttribute" || attr.name.name !== "size") return false;
|
|
1719
|
+
const value = getSizeOrVariantValue(attr);
|
|
1720
|
+
if (!value) return false;
|
|
1721
|
+
return value.startsWith(ICON_SIZE_PREFIX);
|
|
1722
|
+
});
|
|
1723
|
+
}
|
|
1724
|
+
function getElementName(nameNode) {
|
|
1725
|
+
if (nameNode.type === "JSXIdentifier") return nameNode.name;
|
|
1726
|
+
if (nameNode.type === "JSXMemberExpression") return nameNode.property.name;
|
|
1727
|
+
return null;
|
|
1728
|
+
}
|
|
1729
|
+
function hasTextContent(node) {
|
|
1730
|
+
if (!node.children || node.children.length === 0) return false;
|
|
1731
|
+
return node.children.some((child) => {
|
|
1732
|
+
if (child.type === "JSXText") return child.value.trim().length > 0;
|
|
1733
|
+
if (child.type === "JSXExpressionContainer") {
|
|
1734
|
+
const expr = child.expression;
|
|
1735
|
+
if (expr.type === "CallExpression") return true;
|
|
1736
|
+
const staticString = getStaticString(expr);
|
|
1737
|
+
if (staticString !== null) return staticString.trim().length > 0;
|
|
1738
|
+
if (expr.type === "TemplateLiteral") return true;
|
|
1739
|
+
}
|
|
1740
|
+
if (child.type === "JSXElement") {
|
|
1741
|
+
const name = getElementName(child.openingElement.name);
|
|
1742
|
+
if (name && TEXT_CONTENT_NAMES.has(name)) return true;
|
|
1743
|
+
if (hasTextContent(child)) return true;
|
|
1744
|
+
}
|
|
1745
|
+
if (child.type === "JSXFragment") return hasTextContent(child);
|
|
1746
|
+
return false;
|
|
1747
|
+
});
|
|
1748
|
+
}
|
|
1749
|
+
var require_icon_button_label_default = defineRule({
|
|
1750
|
+
createOnce(context) {
|
|
1751
|
+
return {
|
|
1752
|
+
/** @param {import("estree").JSXOpeningElement} node */
|
|
1753
|
+
JSXOpeningElement(node) {
|
|
1754
|
+
const name = getElementName(node.name);
|
|
1755
|
+
if (name === null || !name.endsWith("Button")) return;
|
|
1756
|
+
if (!isIconVariant(node) && !isIconSize(node)) return;
|
|
1757
|
+
if (hasUsableAriaLabel(node)) return;
|
|
1758
|
+
if (hasSlot(node)) return;
|
|
1759
|
+
const parent = node.parent.type === "JSXElement" ? node.parent : null;
|
|
1760
|
+
if (parent && hasTextContent(parent)) return;
|
|
1761
|
+
context.report({
|
|
1762
|
+
loc: node.loc,
|
|
1763
|
+
messageId: "missingAriaLabel",
|
|
1764
|
+
data: { component: name }
|
|
1765
|
+
});
|
|
1766
|
+
} };
|
|
1767
|
+
},
|
|
1768
|
+
meta: {
|
|
1769
|
+
type: "problem",
|
|
1770
|
+
docs: { description: "Require a nonempty aria-label on icon-only *Button JSX (any name ending in Button, including InputGroup.Button) whose size starts with icon or variant is icon; static expression strings and templates count" },
|
|
1771
|
+
schema: [],
|
|
1772
|
+
messages: { missingAriaLabel: "Icon-only <{{component}}> must have an aria-label for accessibility. Add aria-label={t(\"...\")} to provide a screen reader label." }
|
|
1773
|
+
}
|
|
1774
|
+
});
|
|
1775
|
+
//#endregion
|
|
1776
|
+
//#region ../oxlint-plugin/rules/restrict-browser-helper-copy.js
|
|
1777
|
+
const HELPERS = /* @__PURE__ */ new Set([
|
|
1778
|
+
"roleNamed",
|
|
1779
|
+
"headingNamed",
|
|
1780
|
+
"cssVarColor",
|
|
1781
|
+
"textNamed",
|
|
1782
|
+
"textboxNamed",
|
|
1783
|
+
"stampDensity",
|
|
1784
|
+
"px"
|
|
1785
|
+
]);
|
|
1786
|
+
const OWNER_SUFFIX$1 = "/test/themed-browser-render.tsx";
|
|
1787
|
+
/**
|
|
1788
|
+
* @param {string} filename
|
|
1789
|
+
*/
|
|
1790
|
+
function isOwner$1(filename) {
|
|
1791
|
+
return normalizeFilename(filename).endsWith(OWNER_SUFFIX$1);
|
|
1792
|
+
}
|
|
1793
|
+
/**
|
|
1794
|
+
* @param {import("estree").Node | null | undefined} id
|
|
1795
|
+
* @returns {string | null}
|
|
1796
|
+
*/
|
|
1797
|
+
function helperName(id) {
|
|
1798
|
+
if (id?.type !== "Identifier" || !HELPERS.has(id.name)) return null;
|
|
1799
|
+
return id.name;
|
|
1800
|
+
}
|
|
1801
|
+
var restrict_browser_helper_copy_default = defineRule({
|
|
1802
|
+
meta: {
|
|
1803
|
+
type: "problem",
|
|
1804
|
+
docs: { description: "Forbid local declarations of the shared browser-harness helpers (roleNamed, headingNamed, cssVarColor, textNamed, textboxNamed, stampDensity, px) outside packages/ui/test/themed-browser-render.tsx" },
|
|
1805
|
+
messages: { localCopy: "Import {{helper}} from packages/ui/test/themed-browser-render.tsx. Suites do not re-declare harness helpers." },
|
|
1806
|
+
schema: []
|
|
1807
|
+
},
|
|
1808
|
+
defaultOptions: [],
|
|
1809
|
+
createOnce(context) {
|
|
1810
|
+
let skipFile = false;
|
|
1811
|
+
return {
|
|
1812
|
+
Program() {
|
|
1813
|
+
skipFile = isOwner$1(context.filename);
|
|
1814
|
+
},
|
|
1815
|
+
FunctionDeclaration(node) {
|
|
1816
|
+
if (skipFile) return;
|
|
1817
|
+
const helper = helperName(node.id);
|
|
1818
|
+
if (helper === null) return;
|
|
1819
|
+
context.report({
|
|
1820
|
+
node: node.id,
|
|
1821
|
+
messageId: "localCopy",
|
|
1822
|
+
data: { helper }
|
|
1823
|
+
});
|
|
1824
|
+
},
|
|
1825
|
+
VariableDeclarator(node) {
|
|
1826
|
+
if (skipFile) return;
|
|
1827
|
+
const helper = helperName(node.id);
|
|
1828
|
+
if (helper === null) return;
|
|
1829
|
+
context.report({
|
|
1830
|
+
node: node.id,
|
|
1831
|
+
messageId: "localCopy",
|
|
1832
|
+
data: { helper }
|
|
1833
|
+
});
|
|
1834
|
+
}
|
|
1835
|
+
};
|
|
1836
|
+
}
|
|
1837
|
+
});
|
|
1838
|
+
//#endregion
|
|
1839
|
+
//#region ../oxlint-plugin/rules/restrict-focus-ring-call.js
|
|
1840
|
+
const ALLOWED_SUFFIXES = ["/src/styles/utils.ts", "/src/react-aria/link/link.tsx"];
|
|
1841
|
+
/**
|
|
1842
|
+
* @param {string} filename
|
|
1843
|
+
*/
|
|
1844
|
+
function isTestFile(filename) {
|
|
1845
|
+
return filename.endsWith(".test.ts") || filename.endsWith(".test.tsx") || filename.endsWith(".browser.test.tsx") || filename.endsWith(".test-d.tsx");
|
|
1846
|
+
}
|
|
1847
|
+
/**
|
|
1848
|
+
* @param {string} filename
|
|
1849
|
+
*/
|
|
1850
|
+
function isAllowedOwner(filename) {
|
|
1851
|
+
const normalized = normalizeFilename(filename);
|
|
1852
|
+
if (isTestFile(normalized)) return true;
|
|
1853
|
+
return ALLOWED_SUFFIXES.some((suffix) => normalized.endsWith(suffix));
|
|
1854
|
+
}
|
|
1855
|
+
var restrict_focus_ring_call_default = defineRule({
|
|
1856
|
+
meta: {
|
|
1857
|
+
type: "problem",
|
|
1858
|
+
docs: { description: "Forbid focusRing({…}) calls outside styles/utils.ts except the live isFocusVisible call in react-aria/link" },
|
|
1859
|
+
messages: { restrictedCall: "Fixed focus-ring rungs are the constants exported from styles/utils.ts. The only live `focusRing({…})` call is react-aria/link, which passes `isFocusVisible` per render." },
|
|
1860
|
+
schema: []
|
|
1861
|
+
},
|
|
1862
|
+
defaultOptions: [],
|
|
1863
|
+
createOnce(context) {
|
|
1864
|
+
let skipFile = false;
|
|
1865
|
+
return {
|
|
1866
|
+
Program() {
|
|
1867
|
+
skipFile = isAllowedOwner(context.filename);
|
|
1868
|
+
},
|
|
1869
|
+
CallExpression(node) {
|
|
1870
|
+
if (skipFile || !isNamedCall(node.callee, "focusRing")) return;
|
|
1871
|
+
context.report({
|
|
1872
|
+
node,
|
|
1873
|
+
messageId: "restrictedCall"
|
|
1874
|
+
});
|
|
1875
|
+
}
|
|
1876
|
+
};
|
|
1877
|
+
}
|
|
1878
|
+
});
|
|
1879
|
+
//#endregion
|
|
1880
|
+
//#region ../oxlint-plugin/rules/restrict-package-root-from-script.js
|
|
1881
|
+
const OWNER_SUFFIX = "/scripts/paths.ts";
|
|
1882
|
+
/**
|
|
1883
|
+
* @param {string} filename
|
|
1884
|
+
*/
|
|
1885
|
+
function isOwner(filename) {
|
|
1886
|
+
return normalizeFilename(filename).endsWith(OWNER_SUFFIX);
|
|
1887
|
+
}
|
|
1888
|
+
/**
|
|
1889
|
+
* @param {import("estree").Node | null | undefined} node
|
|
1890
|
+
*/
|
|
1891
|
+
function isImportMetaUrl(node) {
|
|
1892
|
+
return node?.type === "MemberExpression" && node.object?.type === "MetaProperty" && node.object.meta?.name === "import" && node.object.property?.name === "meta" && node.property?.type === "Identifier" && node.property.name === "url";
|
|
1893
|
+
}
|
|
1894
|
+
/**
|
|
1895
|
+
* @param {import("estree").Node | null | undefined} node
|
|
1896
|
+
*/
|
|
1897
|
+
function isFileUrlToPathOfImportMetaUrl(node) {
|
|
1898
|
+
return node?.type === "CallExpression" && isNamedCall(node.callee, "fileURLToPath") && node.arguments.length > 0 && isImportMetaUrl(node.arguments[0]);
|
|
1899
|
+
}
|
|
1900
|
+
var restrict_package_root_from_script_default = defineRule({
|
|
1901
|
+
meta: {
|
|
1902
|
+
type: "problem",
|
|
1903
|
+
docs: { description: "Forbid dirname(fileURLToPath(import.meta.url)) outside packages/ui/scripts/paths.ts" },
|
|
1904
|
+
messages: { usePackageRootFromScript: "Locate the package root with packageRootFromScript(import.meta.url). dirname(fileURLToPath(import.meta.url)) lives only in scripts/paths.ts." },
|
|
1905
|
+
schema: []
|
|
1906
|
+
},
|
|
1907
|
+
defaultOptions: [],
|
|
1908
|
+
createOnce(context) {
|
|
1909
|
+
let skipFile = false;
|
|
1910
|
+
return {
|
|
1911
|
+
Program() {
|
|
1912
|
+
skipFile = isOwner(context.filename);
|
|
1913
|
+
},
|
|
1914
|
+
CallExpression(node) {
|
|
1915
|
+
if (skipFile || !isNamedCall(node.callee, "dirname")) return;
|
|
1916
|
+
if (!isFileUrlToPathOfImportMetaUrl(node.arguments[0])) return;
|
|
1917
|
+
context.report({
|
|
1918
|
+
node,
|
|
1919
|
+
messageId: "usePackageRootFromScript"
|
|
1920
|
+
});
|
|
1921
|
+
}
|
|
1922
|
+
};
|
|
1923
|
+
}
|
|
1924
|
+
});
|
|
1925
|
+
//#endregion
|
|
1926
|
+
//#region ../oxlint-plugin/rules/restrict-process-env.js
|
|
1927
|
+
const MESSAGE = "Direct process.env access is forbidden except the process.env.NODE_ENV comparison in the theme validator module.";
|
|
1928
|
+
const ALLOWED_VALIDATOR_SUFFIX = "/src/theme/validate-theme.ts";
|
|
1929
|
+
/**
|
|
1930
|
+
* @param {import("estree").Node | null | undefined} node
|
|
1931
|
+
* @returns {node is import("estree").Identifier}
|
|
1932
|
+
*/
|
|
1933
|
+
function isProcessIdentifier(node) {
|
|
1934
|
+
return node?.type === "Identifier" && node.name === "process";
|
|
1935
|
+
}
|
|
1936
|
+
/**
|
|
1937
|
+
* @param {import("estree").Node | null | undefined} node
|
|
1938
|
+
* @param {boolean} isComputed
|
|
1939
|
+
*/
|
|
1940
|
+
function isEnvProperty(node, isComputed) {
|
|
1941
|
+
if (!isComputed) return node?.type === "Identifier" && node.name === "env";
|
|
1942
|
+
return node?.type === "Literal" && node.value === "env";
|
|
1943
|
+
}
|
|
1944
|
+
/**
|
|
1945
|
+
* @param {import("estree").Node | null | undefined} node
|
|
1946
|
+
* @returns {node is import("estree").MemberExpression}
|
|
1947
|
+
*/
|
|
1948
|
+
function isProcessEnvMemberExpression(node) {
|
|
1949
|
+
return node?.type === "MemberExpression" && isProcessIdentifier(node.object) && isEnvProperty(node.property, node.computed);
|
|
1950
|
+
}
|
|
1951
|
+
/**
|
|
1952
|
+
* @param {import("estree").Node | null | undefined} node
|
|
1953
|
+
* @param {boolean} isComputed
|
|
1954
|
+
*/
|
|
1955
|
+
function isNodeEnvProperty(node, isComputed) {
|
|
1956
|
+
if (!isComputed) return node?.type === "Identifier" && node.name === "NODE_ENV";
|
|
1957
|
+
return node?.type === "Literal" && node.value === "NODE_ENV";
|
|
1958
|
+
}
|
|
1959
|
+
/**
|
|
1960
|
+
* @param {import("estree").MemberExpression} processEnvNode
|
|
1961
|
+
*/
|
|
1962
|
+
function isProcessEnvNodeEnv(processEnvNode) {
|
|
1963
|
+
const parent = processEnvNode.parent;
|
|
1964
|
+
return parent?.type === "MemberExpression" && parent.object === processEnvNode && isNodeEnvProperty(parent.property, parent.computed);
|
|
1965
|
+
}
|
|
1966
|
+
/**
|
|
1967
|
+
* @param {string} operator
|
|
1968
|
+
*/
|
|
1969
|
+
function isComparisonOperator(operator) {
|
|
1970
|
+
return operator === "===" || operator === "!==" || operator === "==" || operator === "!=";
|
|
1971
|
+
}
|
|
1972
|
+
/**
|
|
1973
|
+
* @param {import("estree").MemberExpression} processEnvNode
|
|
1974
|
+
*/
|
|
1975
|
+
function isNodeEnvComparison(processEnvNode) {
|
|
1976
|
+
if (!isProcessEnvNodeEnv(processEnvNode)) return false;
|
|
1977
|
+
const comparison = processEnvNode.parent.parent;
|
|
1978
|
+
return comparison?.type === "BinaryExpression" && isComparisonOperator(comparison.operator);
|
|
1979
|
+
}
|
|
1980
|
+
/**
|
|
1981
|
+
* @param {string} filename
|
|
1982
|
+
*/
|
|
1983
|
+
function isThemeValidatorModule(filename) {
|
|
1984
|
+
return normalizeFilename(filename).endsWith(ALLOWED_VALIDATOR_SUFFIX);
|
|
1985
|
+
}
|
|
1986
|
+
var restrict_process_env_default = defineRule({
|
|
1987
|
+
createOnce(context) {
|
|
1988
|
+
return {
|
|
1989
|
+
/** @param {import("estree").MemberExpression} node */
|
|
1990
|
+
MemberExpression(node) {
|
|
1991
|
+
if (!isProcessEnvMemberExpression(node)) return;
|
|
1992
|
+
if (isThemeValidatorModule(context.filename) && isNodeEnvComparison(node)) return;
|
|
1993
|
+
context.report({
|
|
1994
|
+
loc: node.loc,
|
|
1995
|
+
messageId: "restrictedAccess"
|
|
1996
|
+
});
|
|
1997
|
+
} };
|
|
1998
|
+
},
|
|
1999
|
+
meta: {
|
|
2000
|
+
type: "problem",
|
|
2001
|
+
docs: { description: "Disallow direct process.env access except the process.env.NODE_ENV comparison in the theme validator" },
|
|
2002
|
+
schema: [],
|
|
2003
|
+
messages: { restrictedAccess: MESSAGE }
|
|
2004
|
+
}
|
|
2005
|
+
});
|
|
2006
|
+
//#endregion
|
|
2007
|
+
//#region src/oxlint.ts
|
|
2008
|
+
const plugin = eslintCompatPlugin({
|
|
2009
|
+
meta: { name: "elmera" },
|
|
2010
|
+
rules: {
|
|
2011
|
+
"enforce-variant-standard": enforce_variant_standard_default,
|
|
2012
|
+
"facade-reexport-grammar": facade_reexport_grammar_default,
|
|
2013
|
+
"no-field-part-jsx": no_field_part_jsx_default,
|
|
2014
|
+
"no-hardcoded-density-metrics": no_hardcoded_density_metrics_default,
|
|
2015
|
+
"no-internal-dynamic-import": no_internal_dynamic_import_default,
|
|
2016
|
+
"no-local-focus-ring": no_local_focus_ring_default,
|
|
2017
|
+
"no-primitive-colors": no_primitive_colors_default,
|
|
2018
|
+
"no-rac-outside-quarantine": no_rac_outside_quarantine_default,
|
|
2019
|
+
"no-raw-class-map": no_raw_class_map_default,
|
|
2020
|
+
"no-tailwind-dark-variant": no_tailwind_dark_variant_default,
|
|
2021
|
+
"require-icon-button-label": require_icon_button_label_default,
|
|
2022
|
+
"restrict-browser-helper-copy": restrict_browser_helper_copy_default,
|
|
2023
|
+
"restrict-focus-ring-call": restrict_focus_ring_call_default,
|
|
2024
|
+
"restrict-package-root-from-script": restrict_package_root_from_script_default,
|
|
2025
|
+
"restrict-process-env": restrict_process_env_default
|
|
2026
|
+
}
|
|
2027
|
+
});
|
|
2028
|
+
//#endregion
|
|
2029
|
+
export { plugin as default };
|