@wistia/oxlint-config 1.1.1 → 1.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 +4 -7
- package/configs/javascript.mjs +7 -1
- package/configs/storybook.mjs +8 -2
- package/configs/typescript.mjs +7 -1
- package/index.d.mts +1 -0
- package/index.mjs +1 -0
- package/package.json +3 -1
- package/plugins/custom.mjs +188 -0
- package/rules/custom.mjs +21 -0
- package/rules/import.mjs +1 -1
- package/rules/react-a11y.mjs +35 -35
package/README.md
CHANGED
|
@@ -29,6 +29,8 @@ yarn add -D oxlint-tsgolint
|
|
|
29
29
|
|
|
30
30
|
`oxlint-tsgolint` is a separate Go-based tool that builds your TypeScript program using `typescript-go` and runs type-aware rules (e.g. detecting unhandled promises, unsafe assignments). Without it installed, type-aware rules will not run.
|
|
31
31
|
|
|
32
|
+
_Note: this will likely be unnecessary in the future when Typescript v7 is released._
|
|
33
|
+
|
|
32
34
|
## Quick start
|
|
33
35
|
|
|
34
36
|
Create an `oxlint.config.ts` in your project root:
|
|
@@ -161,12 +163,6 @@ export default defineConfig({
|
|
|
161
163
|
5. Add short description of rule and link to rule documentation in code comments
|
|
162
164
|
6. Never delete a rule — set it to `off` with a comment explaining why
|
|
163
165
|
|
|
164
|
-
## ESLint parity
|
|
165
|
-
|
|
166
|
-
Differences between `@wistia/oxlint-config` and `@wistia/eslint-config`.
|
|
167
|
-
|
|
168
|
-
oxlint and ESLint use different rule name prefixes. This document uses the eslint-config names. See [Rule Name Mapping](#rule-name-mapping) at the bottom for prefix translations.
|
|
169
|
-
|
|
170
166
|
### Why some rules are ESLint-only
|
|
171
167
|
|
|
172
168
|
oxlint's jsPlugins feature can load ESLint plugins that export a standard `{ rules }` object. This works for packages like `eslint-plugin-import-x`, `eslint-plugin-storybook`, etc.
|
|
@@ -179,4 +175,5 @@ Several categories of rules cannot use this approach:
|
|
|
179
175
|
|
|
180
176
|
3. **Rules requiring module resolution** (e.g. `import-x/no-unresolved`, `import-x/extensions`, `import-x/no-rename-default`). In ESLint, these rules use `eslint-import-resolver-typescript` to resolve TypeScript path aliases, barrel re-exports, and extensionless imports. The resolver is configured via ESLint's `settings['import-x/resolver']` — but oxlint's jsPlugin runner does not pass ESLint settings to plugins. Without a resolver, these rules can't find modules and produce thousands of false positives on every import.
|
|
181
177
|
|
|
182
|
-
4. **Old-style ESLint plugins** (e.g. `eslint-plugin-filenames`). These export rules as plain functions instead of objects with a `create` method
|
|
178
|
+
4. **Old-style ESLint plugins** (e.g. `eslint-plugin-filenames`). These export rules as plain functions instead of objects with a `create` method, which oxlint's jsPlugin runner — requiring the modern `{ meta, create }` format — cannot load. The package is also abandoned. Where a rule from such a plugin is worth keeping, it is reimplemented in our own custom local jsPlugin (`custom`) under [`plugins/custom.mjs`](./plugins/custom.mjs).
|
|
179
|
+
- `custom/match-exported` is a faithful port of `eslint-plugin-filenames`' `match-exported`, enabled in `javascriptConfig` + `typescriptConfig`, matching eslint-config. (Its rule ID is namespaced `custom/` rather than `filenames/` to signal it is our own rule, not the upstream plugin.) The upstream `match-regex` and `no-index` rules are not ported — eslint-config disables both.
|
package/configs/javascript.mjs
CHANGED
|
@@ -3,16 +3,22 @@ import { baseRules } from '../rules/base.mjs';
|
|
|
3
3
|
import { importRules } from '../rules/import.mjs';
|
|
4
4
|
import { promiseRules } from '../rules/promise.mjs';
|
|
5
5
|
import { barrelFilesRules } from '../rules/barrel-files.mjs';
|
|
6
|
+
import { customRules } from '../rules/custom.mjs';
|
|
6
7
|
import { unicornRules } from '../rules/unicorn.mjs';
|
|
7
8
|
|
|
8
9
|
export default defineConfig({
|
|
9
10
|
plugins: [...baseRules.plugins, ...importRules.plugins, ...promiseRules.plugins],
|
|
10
|
-
jsPlugins: [
|
|
11
|
+
jsPlugins: [
|
|
12
|
+
...(importRules.jsPlugins || []),
|
|
13
|
+
...(barrelFilesRules.jsPlugins || []),
|
|
14
|
+
...(customRules.jsPlugins || []),
|
|
15
|
+
],
|
|
11
16
|
rules: {
|
|
12
17
|
...baseRules.rules,
|
|
13
18
|
...importRules.rules,
|
|
14
19
|
...promiseRules.rules,
|
|
15
20
|
...barrelFilesRules.rules,
|
|
21
|
+
...customRules.rules,
|
|
16
22
|
...unicornRules.rules,
|
|
17
23
|
},
|
|
18
24
|
});
|
package/configs/storybook.mjs
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { defineConfig } from 'oxlint';
|
|
2
2
|
import { storybookRules } from '../rules/storybook.mjs';
|
|
3
|
+
import { customRules } from '../rules/custom.mjs';
|
|
3
4
|
|
|
4
5
|
export default defineConfig({
|
|
5
6
|
overrides: [
|
|
6
7
|
{
|
|
7
8
|
files: ['**/*.stories.ts', '**/*.stories.tsx', '**/*.stories.js', '**/*.stories.jsx'],
|
|
8
|
-
jsPlugins: storybookRules.jsPlugins,
|
|
9
|
-
rules:
|
|
9
|
+
jsPlugins: [...(storybookRules.jsPlugins || []), ...(customRules.jsPlugins || [])],
|
|
10
|
+
rules: {
|
|
11
|
+
...storybookRules.rules,
|
|
12
|
+
// Decision: stories always default-export an object named `meta`, so
|
|
13
|
+
// the filename can never match the exported name.
|
|
14
|
+
'custom/match-exported': 'off',
|
|
15
|
+
},
|
|
10
16
|
},
|
|
11
17
|
],
|
|
12
18
|
});
|
package/configs/typescript.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { importRules } from '../rules/import.mjs';
|
|
|
4
4
|
import { promiseRules } from '../rules/promise.mjs';
|
|
5
5
|
import { typescriptRules } from '../rules/typescript.mjs';
|
|
6
6
|
import { barrelFilesRules } from '../rules/barrel-files.mjs';
|
|
7
|
+
import { customRules } from '../rules/custom.mjs';
|
|
7
8
|
import { unicornRules } from '../rules/unicorn.mjs';
|
|
8
9
|
|
|
9
10
|
export default defineConfig({
|
|
@@ -17,13 +18,18 @@ export default defineConfig({
|
|
|
17
18
|
...promiseRules.plugins,
|
|
18
19
|
...typescriptRules.plugins,
|
|
19
20
|
],
|
|
20
|
-
jsPlugins: [
|
|
21
|
+
jsPlugins: [
|
|
22
|
+
...(importRules.jsPlugins || []),
|
|
23
|
+
...(barrelFilesRules.jsPlugins || []),
|
|
24
|
+
...(customRules.jsPlugins || []),
|
|
25
|
+
],
|
|
21
26
|
rules: {
|
|
22
27
|
...baseRules.rules,
|
|
23
28
|
...importRules.rules,
|
|
24
29
|
...promiseRules.rules,
|
|
25
30
|
...typescriptRules.rules,
|
|
26
31
|
...barrelFilesRules.rules,
|
|
32
|
+
...customRules.rules,
|
|
27
33
|
...unicornRules.rules,
|
|
28
34
|
},
|
|
29
35
|
});
|
package/index.d.mts
CHANGED
|
@@ -21,6 +21,7 @@ export declare const storybookRules: RuleFile;
|
|
|
21
21
|
export declare const styledComponentsRules: RuleFile;
|
|
22
22
|
export declare const testingLibraryRules: RuleFile;
|
|
23
23
|
export declare const barrelFilesRules: RuleFile;
|
|
24
|
+
export declare const customRules: RuleFile;
|
|
24
25
|
|
|
25
26
|
// Configs
|
|
26
27
|
export declare const javascriptConfig: OxlintConfig;
|
package/index.mjs
CHANGED
|
@@ -13,6 +13,7 @@ export { storybookRules } from './rules/storybook.mjs';
|
|
|
13
13
|
export { styledComponentsRules } from './rules/styled-components.mjs';
|
|
14
14
|
export { testingLibraryRules } from './rules/testing-library.mjs';
|
|
15
15
|
export { barrelFilesRules } from './rules/barrel-files.mjs';
|
|
16
|
+
export { customRules } from './rules/custom.mjs';
|
|
16
17
|
|
|
17
18
|
// Configs
|
|
18
19
|
export { default as javascriptConfig } from './configs/javascript.mjs';
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wistia/oxlint-config",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Wistia's Oxlint configurations",
|
|
5
5
|
"packageManager": "yarn@4.17.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"files": [
|
|
8
8
|
"configs",
|
|
9
9
|
"rules",
|
|
10
|
+
"plugins",
|
|
10
11
|
"index.mjs",
|
|
11
12
|
"index.d.mts"
|
|
12
13
|
],
|
|
@@ -27,6 +28,7 @@
|
|
|
27
28
|
"./testing-library": "./configs/testing-library.mjs",
|
|
28
29
|
"./rules/barrel-files": "./rules/barrel-files.mjs",
|
|
29
30
|
"./rules/base": "./rules/base.mjs",
|
|
31
|
+
"./rules/custom": "./rules/custom.mjs",
|
|
30
32
|
"./rules/import": "./rules/import.mjs",
|
|
31
33
|
"./rules/node": "./rules/node.mjs",
|
|
32
34
|
"./rules/playwright": "./rules/playwright.mjs",
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// `custom` — Wistia's custom oxlint jsPlugin, holding rules with no native
|
|
2
|
+
// oxlint or loadable-ESLint-plugin equivalent.
|
|
3
|
+
//
|
|
4
|
+
// `custom/match-exported` is a faithful port of `match-exported` from the
|
|
5
|
+
// abandoned eslint-plugin-filenames (https://github.com/selaux/eslint-plugin-filenames),
|
|
6
|
+
// rewritten in oxlint's modern `{ meta, create }` plugin format. The original
|
|
7
|
+
// ships its rules as plain functions, which oxlint's jsPlugin runner cannot
|
|
8
|
+
// load — see the ESLint-parity section of the README.
|
|
9
|
+
//
|
|
10
|
+
// The rule ensures a file's name matches the name of its default export
|
|
11
|
+
// (`export default ...`) or `module.exports = ...`. Files with no default
|
|
12
|
+
// export, or an anonymous/object default export, are ignored. `index.*` files
|
|
13
|
+
// are matched against their parent directory name instead.
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
|
|
16
|
+
// --- case transforms (lodash-equivalent for identifier-shaped input) ----------
|
|
17
|
+
const splitWords = (name) =>
|
|
18
|
+
name
|
|
19
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2') // camelCase boundary
|
|
20
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2') // ACRONYMCase boundary
|
|
21
|
+
.replace(/([a-zA-Z])([0-9])/g, '$1 $2') // letter→digit boundary (lodash treats digit runs as words)
|
|
22
|
+
.replace(/([0-9])([a-zA-Z])/g, '$1 $2') // digit→letter boundary
|
|
23
|
+
.split(/[^A-Za-z0-9]+/)
|
|
24
|
+
.filter(Boolean);
|
|
25
|
+
|
|
26
|
+
const upperFirst = (word) => word.charAt(0).toUpperCase() + word.slice(1);
|
|
27
|
+
|
|
28
|
+
const transforms = {
|
|
29
|
+
kebab: (name) =>
|
|
30
|
+
splitWords(name)
|
|
31
|
+
.map((w) => w.toLowerCase())
|
|
32
|
+
.join('-'),
|
|
33
|
+
snake: (name) =>
|
|
34
|
+
splitWords(name)
|
|
35
|
+
.map((w) => w.toLowerCase())
|
|
36
|
+
.join('_'),
|
|
37
|
+
camel: (name) =>
|
|
38
|
+
splitWords(name)
|
|
39
|
+
.map((w, i) => (i === 0 ? w.toLowerCase() : upperFirst(w.toLowerCase())))
|
|
40
|
+
.join(''),
|
|
41
|
+
pascal: (name) =>
|
|
42
|
+
splitWords(name)
|
|
43
|
+
.map((w) => upperFirst(w.toLowerCase()))
|
|
44
|
+
.join(''),
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// --- helpers ported from the original plugin ----------------------------------
|
|
48
|
+
const ignoredFilenames = ['<text>', '<input>'];
|
|
49
|
+
const isIgnoredFilename = (filename) => ignoredFilenames.includes(filename);
|
|
50
|
+
|
|
51
|
+
const parseFilename = (filename) => {
|
|
52
|
+
const ext = path.extname(filename);
|
|
53
|
+
return {
|
|
54
|
+
dir: path.dirname(filename),
|
|
55
|
+
base: path.basename(filename),
|
|
56
|
+
ext,
|
|
57
|
+
name: path.basename(filename, ext),
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const isIndexFile = (parsed) => parsed.name === 'index';
|
|
62
|
+
|
|
63
|
+
const getStringToCheckAgainstExport = (parsed, replacePattern) => {
|
|
64
|
+
const dirArray = parsed.dir.split(path.sep);
|
|
65
|
+
const lastDirectory = dirArray[dirArray.length - 1];
|
|
66
|
+
|
|
67
|
+
if (isIndexFile(parsed)) {
|
|
68
|
+
return lastDirectory;
|
|
69
|
+
}
|
|
70
|
+
return replacePattern ? parsed.name.replace(replacePattern, '') : parsed.name;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const getNodeName = (node, options) => {
|
|
74
|
+
const op = options || [];
|
|
75
|
+
|
|
76
|
+
if (node.type === 'Identifier') {
|
|
77
|
+
return node.name;
|
|
78
|
+
}
|
|
79
|
+
if (node.id && node.id.type === 'Identifier') {
|
|
80
|
+
return node.id.name;
|
|
81
|
+
}
|
|
82
|
+
if (op[2] && node.type === 'CallExpression' && node.callee.type === 'Identifier') {
|
|
83
|
+
return node.callee.name;
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const getExportedName = (programNode, options) => {
|
|
89
|
+
for (const node of programNode.body) {
|
|
90
|
+
// export default ...
|
|
91
|
+
if (node.type === 'ExportDefaultDeclaration') {
|
|
92
|
+
return getNodeName(node.declaration, options);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// module.exports = ...
|
|
96
|
+
if (
|
|
97
|
+
node.type === 'ExpressionStatement' &&
|
|
98
|
+
node.expression.type === 'AssignmentExpression' &&
|
|
99
|
+
node.expression.left.type === 'MemberExpression' &&
|
|
100
|
+
node.expression.left.object.type === 'Identifier' &&
|
|
101
|
+
node.expression.left.object.name === 'module' &&
|
|
102
|
+
node.expression.left.property.type === 'Identifier' &&
|
|
103
|
+
node.expression.left.property.name === 'exports'
|
|
104
|
+
) {
|
|
105
|
+
return getNodeName(node.expression.right, options);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return undefined;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const getTransformsFromOptions = (option) => {
|
|
112
|
+
const usedTransforms = Array.isArray(option) ? option : [option];
|
|
113
|
+
return usedTransforms.map((name) => transforms[name]);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const applyTransforms = (exportedName, usedTransforms) =>
|
|
117
|
+
usedTransforms.map((t) => (t ? t(exportedName) : exportedName));
|
|
118
|
+
|
|
119
|
+
const anyMatch = (expectedExport, transformedNames) =>
|
|
120
|
+
transformedNames.some((name) => name === expectedExport);
|
|
121
|
+
|
|
122
|
+
const getWhatToMatchMessage = (usedTransforms) => {
|
|
123
|
+
if (usedTransforms.length === 1 && !usedTransforms[0]) {
|
|
124
|
+
return 'the exported name';
|
|
125
|
+
}
|
|
126
|
+
if (usedTransforms.length > 1) {
|
|
127
|
+
return 'any of the exported and transformed names';
|
|
128
|
+
}
|
|
129
|
+
return 'the exported and transformed name';
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const matchExported = {
|
|
133
|
+
meta: {
|
|
134
|
+
schema: [
|
|
135
|
+
{
|
|
136
|
+
oneOf: [
|
|
137
|
+
{ enum: ['kebab', 'snake', 'camel', 'pascal', null] },
|
|
138
|
+
{
|
|
139
|
+
type: 'array',
|
|
140
|
+
items: { enum: ['kebab', 'snake', 'camel', 'pascal', null] },
|
|
141
|
+
minItems: 1,
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
},
|
|
145
|
+
{ type: ['string', 'null'] },
|
|
146
|
+
{ type: ['boolean', 'null'] },
|
|
147
|
+
],
|
|
148
|
+
},
|
|
149
|
+
create(context) {
|
|
150
|
+
const options = context.options || [];
|
|
151
|
+
const usedTransforms = getTransformsFromOptions(options[0]);
|
|
152
|
+
const replacePattern = options[1] ? new RegExp(options[1]) : null;
|
|
153
|
+
const filename =
|
|
154
|
+
context.physicalFilename ??
|
|
155
|
+
context.filename ??
|
|
156
|
+
(typeof context.getFilename === 'function' ? context.getFilename() : '');
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
Program(node) {
|
|
160
|
+
if (isIgnoredFilename(filename)) return;
|
|
161
|
+
|
|
162
|
+
const parsed = parseFilename(path.resolve(filename));
|
|
163
|
+
const exportedName = getExportedName(node, options);
|
|
164
|
+
const isExporting = Boolean(exportedName);
|
|
165
|
+
if (!isExporting) return;
|
|
166
|
+
|
|
167
|
+
const expectedExport = getStringToCheckAgainstExport(parsed, replacePattern);
|
|
168
|
+
const transformedNames = applyTransforms(exportedName, usedTransforms);
|
|
169
|
+
const everythingIsIndex = exportedName === 'index' && parsed.name === 'index';
|
|
170
|
+
const matchesExported = anyMatch(expectedExport, transformedNames) || everythingIsIndex;
|
|
171
|
+
|
|
172
|
+
if (matchesExported) return;
|
|
173
|
+
|
|
174
|
+
const exportName = transformedNames.join("', '");
|
|
175
|
+
const message = isIndexFile(parsed)
|
|
176
|
+
? `The directory '${expectedExport}' must be named '${exportName}', after the exported value of its index file.`
|
|
177
|
+
: `Filename '${expectedExport}' must match ${getWhatToMatchMessage(usedTransforms)} '${exportName}'.`;
|
|
178
|
+
|
|
179
|
+
context.report({ node, message });
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
export default {
|
|
186
|
+
meta: { name: 'custom' },
|
|
187
|
+
rules: { 'match-exported': matchExported },
|
|
188
|
+
};
|
package/rules/custom.mjs
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
2
|
+
|
|
3
|
+
// `customRules` configures Wistia's custom local jsPlugin (plugins/custom.mjs),
|
|
4
|
+
// which holds rules that have no native oxlint or loadable-ESLint-plugin
|
|
5
|
+
// equivalent. The plugin path is resolved to an absolute path so it loads both
|
|
6
|
+
// in this repo and from a consumer's node_modules.
|
|
7
|
+
//
|
|
8
|
+
// `custom/match-exported` ports `match-exported` from the abandoned
|
|
9
|
+
// eslint-plugin-filenames (it ships its rules in a legacy format oxlint cannot
|
|
10
|
+
// load). The upstream `match-regex` and `no-index` rules are intentionally not
|
|
11
|
+
// implemented — @wistia/eslint-config disables both.
|
|
12
|
+
const customPlugin = fileURLToPath(new URL('../plugins/custom.mjs', import.meta.url));
|
|
13
|
+
|
|
14
|
+
export const customRules = {
|
|
15
|
+
jsPlugins: [customPlugin],
|
|
16
|
+
rules: {
|
|
17
|
+
// Match Exported Values
|
|
18
|
+
// https://github.com/selaux/eslint-plugin-filenames#matching-exported-values-match-exported
|
|
19
|
+
'custom/match-exported': 'error',
|
|
20
|
+
},
|
|
21
|
+
};
|
package/rules/import.mjs
CHANGED
|
@@ -195,7 +195,7 @@ export const importRules = {
|
|
|
195
195
|
'import-x-js/order': [
|
|
196
196
|
'error',
|
|
197
197
|
{
|
|
198
|
-
groups: [['builtin', 'external', 'internal'], 'index', ['parent', 'sibling']],
|
|
198
|
+
groups: ['type', ['builtin', 'external', 'internal'], 'index', ['parent', 'sibling']],
|
|
199
199
|
'newlines-between': 'never',
|
|
200
200
|
},
|
|
201
201
|
],
|
package/rules/react-a11y.mjs
CHANGED
|
@@ -2,15 +2,15 @@ export const reactA11yRules = {
|
|
|
2
2
|
plugins: ['jsx-a11y'],
|
|
3
3
|
rules: {
|
|
4
4
|
// Enforce that all elements that require alternative text have meaningful information
|
|
5
|
-
// https://oxc.rs/docs/guide/usage/linter/rules/
|
|
5
|
+
// https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/alt-text.html
|
|
6
6
|
'jsx_a11y/alt-text': 'error',
|
|
7
7
|
|
|
8
8
|
// Enforce that anchors have content
|
|
9
|
-
// https://oxc.rs/docs/guide/usage/linter/rules/
|
|
9
|
+
// https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/anchor-has-content.html
|
|
10
10
|
'jsx_a11y/anchor-has-content': 'error',
|
|
11
11
|
|
|
12
12
|
// Enforce all anchors are valid, navigable elements
|
|
13
|
-
// https://oxc.rs/docs/guide/usage/linter/rules/
|
|
13
|
+
// https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/anchor-is-valid.html
|
|
14
14
|
'jsx_a11y/anchor-is-valid': [
|
|
15
15
|
'error',
|
|
16
16
|
{
|
|
@@ -21,40 +21,40 @@ export const reactA11yRules = {
|
|
|
21
21
|
],
|
|
22
22
|
|
|
23
23
|
// Enforce that elements with aria-activedescendant have tabindex
|
|
24
|
-
// https://oxc.rs/docs/guide/usage/linter/rules/
|
|
24
|
+
// https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/aria-activedescendant-has-tabindex.html
|
|
25
25
|
'jsx_a11y/aria-activedescendant-has-tabindex': 'error',
|
|
26
26
|
|
|
27
27
|
// Enforce that elements have valid aria-* props
|
|
28
|
-
// https://oxc.rs/docs/guide/usage/linter/rules/
|
|
28
|
+
// https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/aria-props.html
|
|
29
29
|
'jsx_a11y/aria-props': 'error',
|
|
30
30
|
|
|
31
31
|
// Enforce that ARIA state and property values are valid
|
|
32
|
-
// https://oxc.rs/docs/guide/usage/linter/rules/
|
|
32
|
+
// https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/aria-proptypes.html
|
|
33
33
|
'jsx_a11y/aria-proptypes': 'error',
|
|
34
34
|
|
|
35
35
|
// Enforce that elements with ARIA roles have all required attributes for that role
|
|
36
|
-
// https://oxc.rs/docs/guide/usage/linter/rules/
|
|
36
|
+
// https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/role-has-required-aria-props.html
|
|
37
37
|
'jsx_a11y/role-has-required-aria-props': 'error',
|
|
38
38
|
|
|
39
39
|
// Enforce that elements with explicit or implicit roles defined contain only aria-* properties supported by that role
|
|
40
|
-
// https://oxc.rs/docs/guide/usage/linter/rules/
|
|
40
|
+
// https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/role-supports-aria-props.html
|
|
41
41
|
'jsx_a11y/role-supports-aria-props': 'error',
|
|
42
42
|
|
|
43
43
|
// Enforce that elements with ARIA roles must use a valid, non-abstract ARIA role
|
|
44
|
-
// https://oxc.rs/docs/guide/usage/linter/rules/
|
|
44
|
+
// https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/aria-role.html
|
|
45
45
|
'jsx_a11y/aria-role': 'error',
|
|
46
46
|
|
|
47
47
|
// Enforce that certain elements don't have ARIA roles, states, or properties
|
|
48
|
-
// https://oxc.rs/docs/guide/usage/linter/rules/
|
|
48
|
+
// https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/aria-unsupported-elements.html
|
|
49
49
|
'jsx_a11y/aria-unsupported-elements': 'error',
|
|
50
50
|
|
|
51
51
|
// Enforce that autocomplete attribute is correct
|
|
52
|
-
// https://oxc.rs/docs/guide/usage/linter/rules/
|
|
52
|
+
// https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/autocomplete-valid.html
|
|
53
53
|
// Decision: too opinionated for general use
|
|
54
54
|
'jsx_a11y/autocomplete-valid': 'off',
|
|
55
55
|
|
|
56
56
|
// Enforce that a control (an interactive element) has a text label
|
|
57
|
-
// https://oxc.rs/docs/guide/usage/linter/rules/
|
|
57
|
+
// https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/control-has-associated-label.html
|
|
58
58
|
'jsx_a11y/control-has-associated-label': [
|
|
59
59
|
'error',
|
|
60
60
|
{
|
|
@@ -78,27 +78,27 @@ export const reactA11yRules = {
|
|
|
78
78
|
],
|
|
79
79
|
|
|
80
80
|
// Enforce a clickable non-interactive element has at least one keyboard event listener
|
|
81
|
-
// https://oxc.rs/docs/guide/usage/linter/rules/
|
|
81
|
+
// https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/click-events-have-key-events.html
|
|
82
82
|
'jsx_a11y/click-events-have-key-events': 'error',
|
|
83
83
|
|
|
84
84
|
// Enforce heading elements have content
|
|
85
|
-
// https://oxc.rs/docs/guide/usage/linter/rules/
|
|
85
|
+
// https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/heading-has-content.html
|
|
86
86
|
'jsx_a11y/heading-has-content': ['error', { components: [''] }],
|
|
87
87
|
|
|
88
88
|
// Enforce <html> element has lang prop
|
|
89
|
-
// https://oxc.rs/docs/guide/usage/linter/rules/
|
|
89
|
+
// https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/html-has-lang.html
|
|
90
90
|
'jsx_a11y/html-has-lang': 'error',
|
|
91
91
|
|
|
92
92
|
// Enforce iframe elements have a title attribute
|
|
93
|
-
// https://oxc.rs/docs/guide/usage/linter/rules/
|
|
93
|
+
// https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/iframe-has-title.html
|
|
94
94
|
'jsx_a11y/iframe-has-title': 'error',
|
|
95
95
|
|
|
96
96
|
// Enforce <img> alt prop does not contain the word "image", "picture", or "photo"
|
|
97
|
-
// https://oxc.rs/docs/guide/usage/linter/rules/
|
|
97
|
+
// https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/img-redundant-alt.html
|
|
98
98
|
'jsx_a11y/img-redundant-alt': 'error',
|
|
99
99
|
|
|
100
100
|
// Enforce that a label tag has a text label and an associated control
|
|
101
|
-
// https://oxc.rs/docs/guide/usage/linter/rules/
|
|
101
|
+
// https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/label-has-associated-control.html
|
|
102
102
|
'jsx_a11y/label-has-associated-control': [
|
|
103
103
|
'error',
|
|
104
104
|
{
|
|
@@ -111,31 +111,31 @@ export const reactA11yRules = {
|
|
|
111
111
|
],
|
|
112
112
|
|
|
113
113
|
// Enforce lang attribute has a valid value
|
|
114
|
-
// https://oxc.rs/docs/guide/usage/linter/rules/
|
|
114
|
+
// https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/lang.html
|
|
115
115
|
'jsx_a11y/lang': 'error',
|
|
116
116
|
|
|
117
117
|
// Enforce that media elements have captions
|
|
118
|
-
// https://oxc.rs/docs/guide/usage/linter/rules/
|
|
118
|
+
// https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/media-has-caption.html
|
|
119
119
|
'jsx_a11y/media-has-caption': 'error',
|
|
120
120
|
|
|
121
121
|
// Enforce that onMouseOver/onMouseOut are accompanied by onFocus/onBlur
|
|
122
|
-
// https://oxc.rs/docs/guide/usage/linter/rules/
|
|
122
|
+
// https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/mouse-events-have-key-events.html
|
|
123
123
|
'jsx_a11y/mouse-events-have-key-events': 'error',
|
|
124
124
|
|
|
125
125
|
// Enforce that the accessKey prop is not used on any element
|
|
126
|
-
// https://oxc.rs/docs/guide/usage/linter/rules/
|
|
126
|
+
// https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/no-access-key.html
|
|
127
127
|
'jsx_a11y/no-access-key': 'error',
|
|
128
128
|
|
|
129
129
|
// Enforce autoFocus prop is not used
|
|
130
|
-
// https://oxc.rs/docs/guide/usage/linter/rules/
|
|
130
|
+
// https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/no-autofocus.html
|
|
131
131
|
'jsx_a11y/no-autofocus': ['error', { ignoreNonDOM: true }],
|
|
132
132
|
|
|
133
133
|
// Enforce distracting elements are not used
|
|
134
|
-
// https://oxc.rs/docs/guide/usage/linter/rules/
|
|
134
|
+
// https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/no-distracting-elements.html
|
|
135
135
|
'jsx_a11y/no-distracting-elements': 'error',
|
|
136
136
|
|
|
137
137
|
// Enforce tabIndex value is not greater than zero
|
|
138
|
-
// https://oxc.rs/docs/guide/usage/linter/rules/
|
|
138
|
+
// https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/tabindex-no-positive.html
|
|
139
139
|
'jsx_a11y/tabindex-no-positive': 'error',
|
|
140
140
|
|
|
141
141
|
// Elements with an interactive role and interaction handlers must be focusable
|
|
@@ -143,7 +143,7 @@ export const reactA11yRules = {
|
|
|
143
143
|
'jsx_a11y/interactive-supports-focus': 'error',
|
|
144
144
|
|
|
145
145
|
// Ensure interactive elements are not assigned non-interactive roles
|
|
146
|
-
// https://oxc.rs/docs/guide/usage/linter/rules/
|
|
146
|
+
// https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/no-noninteractive-tabindex.html
|
|
147
147
|
'jsx_a11y/no-noninteractive-tabindex': [
|
|
148
148
|
'error',
|
|
149
149
|
{
|
|
@@ -153,21 +153,21 @@ export const reactA11yRules = {
|
|
|
153
153
|
],
|
|
154
154
|
|
|
155
155
|
// Ensure interactive elements are not assigned non-interactive roles
|
|
156
|
-
// https://oxc.rs/docs/guide/usage/linter/rules/
|
|
156
|
+
// https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/no-interactive-element-to-noninteractive-role.html
|
|
157
157
|
'jsx_a11y/no-interactive-element-to-noninteractive-role': [
|
|
158
158
|
'error',
|
|
159
159
|
{ tr: ['none', 'presentation'] },
|
|
160
160
|
],
|
|
161
161
|
|
|
162
162
|
// Enforce that non-interactive elements do not have interaction handlers
|
|
163
|
-
// https://oxc.rs/docs/guide/usage/linter/rules/
|
|
163
|
+
// https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/no-noninteractive-element-interactions.html
|
|
164
164
|
'jsx_a11y/no-noninteractive-element-interactions': [
|
|
165
165
|
'error',
|
|
166
166
|
{ handlers: ['onClick', 'onMouseDown', 'onMouseUp', 'onKeyPress', 'onKeyDown', 'onKeyUp'] },
|
|
167
167
|
],
|
|
168
168
|
|
|
169
169
|
// Enforce that non-interactive elements are not assigned interactive roles
|
|
170
|
-
// https://oxc.rs/docs/guide/usage/linter/rules/
|
|
170
|
+
// https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/no-noninteractive-element-to-interactive-role.html
|
|
171
171
|
'jsx_a11y/no-noninteractive-element-to-interactive-role': [
|
|
172
172
|
'error',
|
|
173
173
|
{
|
|
@@ -180,27 +180,27 @@ export const reactA11yRules = {
|
|
|
180
180
|
],
|
|
181
181
|
|
|
182
182
|
// Enforce explicit role is not redundant with implicit role of the element
|
|
183
|
-
// https://oxc.rs/docs/guide/usage/linter/rules/
|
|
183
|
+
// https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/no-redundant-roles.html
|
|
184
184
|
'jsx_a11y/no-redundant-roles': 'error',
|
|
185
185
|
|
|
186
186
|
// Enforce that non-interactive, visible elements with click handlers use the role attribute
|
|
187
|
-
// https://oxc.rs/docs/guide/usage/linter/rules/
|
|
187
|
+
// https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/no-static-element-interactions.html
|
|
188
188
|
'jsx_a11y/no-static-element-interactions': 'error',
|
|
189
189
|
|
|
190
190
|
// Enforce scope prop is only used on <th> elements
|
|
191
|
-
// https://oxc.rs/docs/guide/usage/linter/rules/
|
|
191
|
+
// https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/scope.html
|
|
192
192
|
'jsx_a11y/scope': 'error',
|
|
193
193
|
|
|
194
194
|
// Enforce that anchor elements have non-ambiguous text content
|
|
195
|
-
// https://oxc.rs/docs/guide/usage/linter/rules/
|
|
195
|
+
// https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/anchor-ambiguous-text.html
|
|
196
196
|
'jsx_a11y/anchor-ambiguous-text': 'error',
|
|
197
197
|
|
|
198
198
|
// Enforce that aria-hidden="true" is not set on focusable elements
|
|
199
|
-
// https://oxc.rs/docs/guide/usage/linter/rules/
|
|
199
|
+
// https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/no-aria-hidden-on-focusable.html
|
|
200
200
|
'jsx_a11y/no-aria-hidden-on-focusable': 'error',
|
|
201
201
|
|
|
202
202
|
// Prefer semantic HTML elements over role attributes
|
|
203
|
-
// https://oxc.rs/docs/guide/usage/linter/rules/
|
|
203
|
+
// https://oxc.rs/docs/guide/usage/linter/rules/jsx_a11y/prefer-tag-over-role.html
|
|
204
204
|
// Decision: left to implementer
|
|
205
205
|
'jsx_a11y/prefer-tag-over-role': 'off',
|
|
206
206
|
},
|