@blogic-cz/oxc-config 1.0.0 → 1.1.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/.oxlintrc.ts.json +29 -2
- package/package.json +7 -1
- package/plugins/enforce-props-type-name.js +136 -0
- package/plugins/enforce-props-type-name.ts +192 -0
- package/plugins/max-file-lines.js +133 -0
- package/plugins/max-file-lines.ts +201 -0
package/.oxlintrc.ts.json
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://raw.githubusercontent.com/niconiconainu/oxc/HEAD/npm/oxc-types/src/generated/schema/oxlintrc.json",
|
|
3
|
+
"jsPlugins": [
|
|
4
|
+
"./plugins/enforce-props-type-name.js",
|
|
5
|
+
"./plugins/max-file-lines.js"
|
|
6
|
+
],
|
|
3
7
|
"plugins": [
|
|
4
8
|
"import",
|
|
5
9
|
"node",
|
|
@@ -50,9 +54,27 @@
|
|
|
50
54
|
"unicorn/prefer-set-has": "error",
|
|
51
55
|
"unicorn/require-module-specifiers": "off",
|
|
52
56
|
"unicorn/throw-new-error": "error",
|
|
57
|
+
"node/handle-callback-err": "error",
|
|
58
|
+
"typescript/no-unnecessary-type-conversion": "error",
|
|
53
59
|
"vitest/no-import-node-test": "error",
|
|
60
|
+
"vitest/prefer-strict-boolean-matchers": "error",
|
|
54
61
|
"oxc/no-map-spread": "error",
|
|
55
|
-
"promise/always-return": "error"
|
|
62
|
+
"promise/always-return": "error",
|
|
63
|
+
"file-quality/kebab-case-filename": "error",
|
|
64
|
+
"file-quality/max-file-lines": [
|
|
65
|
+
"off",
|
|
66
|
+
{
|
|
67
|
+
"maxLines": 500,
|
|
68
|
+
"extensions": [".tsx"]
|
|
69
|
+
}
|
|
70
|
+
],
|
|
71
|
+
"file-quality/no-barrel-files": "error",
|
|
72
|
+
"naming/enforce-props-type-name": "error",
|
|
73
|
+
"naming/no-default-parameter-values": "error",
|
|
74
|
+
"naming/no-console-in-server": "error",
|
|
75
|
+
"naming/no-dynamic-type-import": "error",
|
|
76
|
+
"naming/no-server-logger-in-client": "error",
|
|
77
|
+
"naming/no-undefined-parameter-union": "error"
|
|
56
78
|
},
|
|
57
79
|
"overrides": [
|
|
58
80
|
{
|
|
@@ -90,6 +112,9 @@
|
|
|
90
112
|
"eslint/no-shadow": "off",
|
|
91
113
|
"eslint/require-await": "off",
|
|
92
114
|
"import/no-relative-parent-imports": "off",
|
|
115
|
+
"naming/enforce-props-type-name": "off",
|
|
116
|
+
"naming/no-default-parameter-values": "off",
|
|
117
|
+
"naming/no-undefined-parameter-union": "off",
|
|
93
118
|
"typescript/no-unsafe-type-assertion": "off",
|
|
94
119
|
"unicorn/no-array-sort": "off"
|
|
95
120
|
}
|
|
@@ -102,6 +127,7 @@
|
|
|
102
127
|
"rules": {
|
|
103
128
|
"eslint/no-new": "off",
|
|
104
129
|
"import/no-relative-parent-imports": "off",
|
|
130
|
+
"naming/no-default-parameter-values": "off",
|
|
105
131
|
"typescript/consistent-type-definitions": "off",
|
|
106
132
|
"typescript/no-import-type-side-effects": "off",
|
|
107
133
|
"typescript/no-non-null-assertion": "off"
|
|
@@ -121,7 +147,8 @@
|
|
|
121
147
|
"packages/logger/src/**"
|
|
122
148
|
],
|
|
123
149
|
"rules": {
|
|
124
|
-
"eslint/require-await": "off"
|
|
150
|
+
"eslint/require-await": "off",
|
|
151
|
+
"naming/no-default-parameter-values": "off"
|
|
125
152
|
}
|
|
126
153
|
}
|
|
127
154
|
]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blogic-cz/oxc-config",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Shared oxlint and oxfmt configs for blogic projects — layered TypeScript, React, and formatter presets",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"oxlint",
|
|
@@ -24,9 +24,15 @@
|
|
|
24
24
|
".oxlintrc.ts.json",
|
|
25
25
|
".oxlintrc.ts-react.json",
|
|
26
26
|
".oxfmtrc.base.jsonc",
|
|
27
|
+
"plugins/*.js",
|
|
28
|
+
"plugins/*.ts",
|
|
27
29
|
"README.md",
|
|
28
30
|
"LICENSE"
|
|
29
31
|
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "bun build plugins/enforce-props-type-name.ts --outdir plugins --outfile enforce-props-type-name.js && bun build plugins/max-file-lines.ts --outdir plugins --outfile max-file-lines.js",
|
|
34
|
+
"prepublishOnly": "bun run build"
|
|
35
|
+
},
|
|
30
36
|
"publishConfig": {
|
|
31
37
|
"access": "public"
|
|
32
38
|
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// plugins/enforce-props-type-name.ts
|
|
2
|
+
var isFunctionNode = (type) => type === "FunctionDeclaration" || type === "FunctionExpression" || type === "ArrowFunctionExpression" || type === "TSDeclareFunction";
|
|
3
|
+
var plugin = {
|
|
4
|
+
meta: {
|
|
5
|
+
name: "naming"
|
|
6
|
+
},
|
|
7
|
+
rules: {
|
|
8
|
+
"enforce-props-type-name": {
|
|
9
|
+
meta: {
|
|
10
|
+
schema: []
|
|
11
|
+
},
|
|
12
|
+
create(context) {
|
|
13
|
+
return {
|
|
14
|
+
TSTypeAliasDeclaration(node) {
|
|
15
|
+
if (!context.filename.endsWith(".tsx")) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const name = node.id.name;
|
|
19
|
+
if (name.endsWith("Props") && name !== "Props") {
|
|
20
|
+
context.report({
|
|
21
|
+
message: `Props type must be named "Props", not "${name}". Use \`type Props = { ... }\` instead.`,
|
|
22
|
+
node: node.id
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"no-dynamic-type-import": {
|
|
30
|
+
meta: {
|
|
31
|
+
schema: []
|
|
32
|
+
},
|
|
33
|
+
create(context) {
|
|
34
|
+
return {
|
|
35
|
+
TSImportType(node) {
|
|
36
|
+
context.report({
|
|
37
|
+
message: 'Use regular `import type` instead of dynamic `import("...")` type syntax.',
|
|
38
|
+
node
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"no-server-logger-in-client": {
|
|
45
|
+
meta: {
|
|
46
|
+
schema: []
|
|
47
|
+
},
|
|
48
|
+
create(context) {
|
|
49
|
+
return {
|
|
50
|
+
ImportDeclaration(node) {
|
|
51
|
+
const source = node.source.value;
|
|
52
|
+
if ((source.endsWith("/logger") || source === "pino") && context.filename.endsWith(".tsx")) {
|
|
53
|
+
context.report({
|
|
54
|
+
message: `Do not import "${source}" in client-side (.tsx) files. Use console.log/console.error instead.`,
|
|
55
|
+
node
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"no-console-in-server": {
|
|
63
|
+
meta: {
|
|
64
|
+
schema: []
|
|
65
|
+
},
|
|
66
|
+
create(context) {
|
|
67
|
+
return {
|
|
68
|
+
MemberExpression(node) {
|
|
69
|
+
const filename = context.filename;
|
|
70
|
+
const isServerFile = filename.endsWith(".ts") && !filename.endsWith(".tsx");
|
|
71
|
+
const isWebAppServer = filename.includes("apps/web-app/src/");
|
|
72
|
+
const isExcluded = filename.includes(".test.ts") || filename.includes("/jobs/") || filename.includes("/scripts/");
|
|
73
|
+
if (isServerFile && isWebAppServer && !isExcluded && node.object.type === "Identifier" && node.object.name === "console") {
|
|
74
|
+
context.report({
|
|
75
|
+
message: "Do not use console.* in server-side code. Use the project logger package instead.",
|
|
76
|
+
node
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
"no-default-parameter-values": {
|
|
84
|
+
meta: {
|
|
85
|
+
schema: []
|
|
86
|
+
},
|
|
87
|
+
create(context) {
|
|
88
|
+
return {
|
|
89
|
+
AssignmentPattern(node) {
|
|
90
|
+
const parentType = node.parent.type;
|
|
91
|
+
const isFunctionParamDefault = parentType === "FunctionDeclaration" || parentType === "FunctionExpression" || parentType === "ArrowFunctionExpression" || parentType === "TSParameterProperty";
|
|
92
|
+
if (!isFunctionParamDefault) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
context.report({
|
|
96
|
+
message: "Default parameter values are not allowed. Require the caller to pass the value explicitly.",
|
|
97
|
+
node
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
"no-undefined-parameter-union": {
|
|
104
|
+
meta: {
|
|
105
|
+
schema: []
|
|
106
|
+
},
|
|
107
|
+
create(context) {
|
|
108
|
+
return {
|
|
109
|
+
TSUndefinedKeyword(node) {
|
|
110
|
+
if (node.parent.type !== "TSUnionType") {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (node.parent.parent.type !== "TSTypeAnnotation") {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const annotationTarget = node.parent.parent.parent;
|
|
117
|
+
const parentType = annotationTarget.parent.type;
|
|
118
|
+
const isDirectFunctionParam = parentType && isFunctionNode(parentType);
|
|
119
|
+
const isTsParameterProperty = annotationTarget.type === "Identifier" && parentType === "TSParameterProperty" && annotationTarget.parent.parent.type === "FunctionExpression" && annotationTarget.parent.parent.parent.type === "MethodDefinition";
|
|
120
|
+
if (!isDirectFunctionParam && !isTsParameterProperty) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
context.report({
|
|
124
|
+
message: "Avoid `| undefined` in parameter types. Use explicit `| null` (or an optional parameter when intended).",
|
|
125
|
+
node
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
var enforce_props_type_name_default = plugin;
|
|
134
|
+
export {
|
|
135
|
+
enforce_props_type_name_default as default
|
|
136
|
+
};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import type { Plugin } from "#oxlint/plugins";
|
|
2
|
+
|
|
3
|
+
const isFunctionNode = (type: string): boolean =>
|
|
4
|
+
type === "FunctionDeclaration" ||
|
|
5
|
+
type === "FunctionExpression" ||
|
|
6
|
+
type === "ArrowFunctionExpression" ||
|
|
7
|
+
type === "TSDeclareFunction";
|
|
8
|
+
|
|
9
|
+
const plugin: Plugin = {
|
|
10
|
+
meta: {
|
|
11
|
+
name: "naming",
|
|
12
|
+
},
|
|
13
|
+
rules: {
|
|
14
|
+
"enforce-props-type-name": {
|
|
15
|
+
meta: {
|
|
16
|
+
schema: [],
|
|
17
|
+
},
|
|
18
|
+
create(context) {
|
|
19
|
+
return {
|
|
20
|
+
TSTypeAliasDeclaration(node) {
|
|
21
|
+
if (!context.filename.endsWith(".tsx")) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const name = node.id.name;
|
|
26
|
+
if (
|
|
27
|
+
name.endsWith("Props") &&
|
|
28
|
+
name !== "Props"
|
|
29
|
+
) {
|
|
30
|
+
context.report({
|
|
31
|
+
message: `Props type must be named "Props", not "${name}". Use \`type Props = { ... }\` instead.`,
|
|
32
|
+
node: node.id,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
"no-dynamic-type-import": {
|
|
40
|
+
meta: {
|
|
41
|
+
schema: [],
|
|
42
|
+
},
|
|
43
|
+
create(context) {
|
|
44
|
+
return {
|
|
45
|
+
TSImportType(node) {
|
|
46
|
+
context.report({
|
|
47
|
+
message:
|
|
48
|
+
'Use regular `import type` instead of dynamic `import("...")` type syntax.',
|
|
49
|
+
node,
|
|
50
|
+
});
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
"no-server-logger-in-client": {
|
|
56
|
+
meta: {
|
|
57
|
+
schema: [],
|
|
58
|
+
},
|
|
59
|
+
create(context) {
|
|
60
|
+
return {
|
|
61
|
+
ImportDeclaration(node) {
|
|
62
|
+
const source = node.source.value;
|
|
63
|
+
if (
|
|
64
|
+
(source.endsWith("/logger") ||
|
|
65
|
+
source === "pino") &&
|
|
66
|
+
context.filename.endsWith(".tsx")
|
|
67
|
+
) {
|
|
68
|
+
context.report({
|
|
69
|
+
message: `Do not import "${source}" in client-side (.tsx) files. Use console.log/console.error instead.`,
|
|
70
|
+
node,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
"no-console-in-server": {
|
|
78
|
+
meta: {
|
|
79
|
+
schema: [],
|
|
80
|
+
},
|
|
81
|
+
create(context) {
|
|
82
|
+
return {
|
|
83
|
+
MemberExpression(node) {
|
|
84
|
+
const filename = context.filename;
|
|
85
|
+
const isServerFile =
|
|
86
|
+
filename.endsWith(".ts") &&
|
|
87
|
+
!filename.endsWith(".tsx");
|
|
88
|
+
const isWebAppServer = filename.includes(
|
|
89
|
+
"apps/web-app/src/"
|
|
90
|
+
);
|
|
91
|
+
const isExcluded =
|
|
92
|
+
filename.includes(".test.ts") ||
|
|
93
|
+
filename.includes("/jobs/") ||
|
|
94
|
+
filename.includes("/scripts/");
|
|
95
|
+
|
|
96
|
+
if (
|
|
97
|
+
isServerFile &&
|
|
98
|
+
isWebAppServer &&
|
|
99
|
+
!isExcluded &&
|
|
100
|
+
node.object.type === "Identifier" &&
|
|
101
|
+
node.object.name === "console"
|
|
102
|
+
) {
|
|
103
|
+
context.report({
|
|
104
|
+
message:
|
|
105
|
+
"Do not use console.* in server-side code. Use the project logger package instead.",
|
|
106
|
+
node,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
"no-default-parameter-values": {
|
|
114
|
+
meta: {
|
|
115
|
+
schema: [],
|
|
116
|
+
},
|
|
117
|
+
create(context) {
|
|
118
|
+
return {
|
|
119
|
+
AssignmentPattern(node) {
|
|
120
|
+
const parentType = node.parent.type;
|
|
121
|
+
|
|
122
|
+
const isFunctionParamDefault =
|
|
123
|
+
parentType === "FunctionDeclaration" ||
|
|
124
|
+
parentType === "FunctionExpression" ||
|
|
125
|
+
parentType === "ArrowFunctionExpression" ||
|
|
126
|
+
parentType === "TSParameterProperty";
|
|
127
|
+
|
|
128
|
+
if (!isFunctionParamDefault) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
context.report({
|
|
133
|
+
message:
|
|
134
|
+
"Default parameter values are not allowed. Require the caller to pass the value explicitly.",
|
|
135
|
+
node,
|
|
136
|
+
});
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
"no-undefined-parameter-union": {
|
|
142
|
+
meta: {
|
|
143
|
+
schema: [],
|
|
144
|
+
},
|
|
145
|
+
create(context) {
|
|
146
|
+
return {
|
|
147
|
+
TSUndefinedKeyword(node) {
|
|
148
|
+
if (node.parent.type !== "TSUnionType") {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (
|
|
153
|
+
node.parent.parent.type !== "TSTypeAnnotation"
|
|
154
|
+
) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const annotationTarget =
|
|
159
|
+
node.parent.parent.parent;
|
|
160
|
+
const parentType = annotationTarget.parent.type;
|
|
161
|
+
|
|
162
|
+
const isDirectFunctionParam =
|
|
163
|
+
parentType && isFunctionNode(parentType);
|
|
164
|
+
|
|
165
|
+
const isTsParameterProperty =
|
|
166
|
+
annotationTarget.type === "Identifier" &&
|
|
167
|
+
parentType === "TSParameterProperty" &&
|
|
168
|
+
annotationTarget.parent.parent.type ===
|
|
169
|
+
"FunctionExpression" &&
|
|
170
|
+
annotationTarget.parent.parent.parent.type ===
|
|
171
|
+
"MethodDefinition";
|
|
172
|
+
|
|
173
|
+
if (
|
|
174
|
+
!isDirectFunctionParam &&
|
|
175
|
+
!isTsParameterProperty
|
|
176
|
+
) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
context.report({
|
|
181
|
+
message:
|
|
182
|
+
"Avoid `| undefined` in parameter types. Use explicit `| null` (or an optional parameter when intended).",
|
|
183
|
+
node,
|
|
184
|
+
});
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
export default plugin;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// plugins/max-file-lines.ts
|
|
2
|
+
function parseOptions(value) {
|
|
3
|
+
if (typeof value !== "object" || value === null) {
|
|
4
|
+
return { maxLines: 500, extensions: [".tsx"] };
|
|
5
|
+
}
|
|
6
|
+
const maxLines = typeof value.maxLines === "number" ? value.maxLines : 500;
|
|
7
|
+
const extensions = Array.isArray(value.extensions) ? value.extensions.filter((extension) => typeof extension === "string") : [".tsx"];
|
|
8
|
+
return {
|
|
9
|
+
maxLines,
|
|
10
|
+
extensions: extensions.length > 0 ? extensions : [".tsx"]
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
var plugin = {
|
|
14
|
+
meta: {
|
|
15
|
+
name: "file-quality"
|
|
16
|
+
},
|
|
17
|
+
rules: {
|
|
18
|
+
"max-file-lines": {
|
|
19
|
+
meta: {
|
|
20
|
+
defaultOptions: [
|
|
21
|
+
{
|
|
22
|
+
maxLines: 500,
|
|
23
|
+
extensions: [".tsx"]
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
schema: [
|
|
27
|
+
{
|
|
28
|
+
type: "object",
|
|
29
|
+
default: {},
|
|
30
|
+
properties: {
|
|
31
|
+
maxLines: { type: "number", default: 500 },
|
|
32
|
+
extensions: {
|
|
33
|
+
type: "array",
|
|
34
|
+
items: { type: "string" },
|
|
35
|
+
default: [".tsx"]
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
additionalProperties: false
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
create(context) {
|
|
43
|
+
return {
|
|
44
|
+
Program() {
|
|
45
|
+
const options = parseOptions(context.options[0]);
|
|
46
|
+
const { maxLines, extensions } = options;
|
|
47
|
+
if (!extensions.some((ext) => context.filename.endsWith(ext))) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const lineCount = context.sourceCode.lines.length;
|
|
51
|
+
if (lineCount <= maxLines) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
context.report({
|
|
55
|
+
message: `File has ${lineCount} lines (max ${maxLines}). Consider splitting into smaller modules.`,
|
|
56
|
+
node: context.sourceCode.ast
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"no-barrel-files": {
|
|
63
|
+
meta: {
|
|
64
|
+
schema: []
|
|
65
|
+
},
|
|
66
|
+
create(context) {
|
|
67
|
+
return {
|
|
68
|
+
Program(node) {
|
|
69
|
+
const filename = context.filename;
|
|
70
|
+
if (!filename.endsWith("/index.ts")) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (/packages\/[^/]+\/src\//.test(filename)) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const body = node.body;
|
|
77
|
+
if (body.length === 0) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const isBarrel = body.every((stmt) => stmt.type === "ExportNamedDeclaration" && stmt.source !== null || stmt.type === "ExportAllDeclaration" || stmt.type === "ImportDeclaration");
|
|
81
|
+
if (isBarrel) {
|
|
82
|
+
context.report({
|
|
83
|
+
message: "Barrel files (index.ts with only re-exports) are not allowed. Import directly from source files.",
|
|
84
|
+
node
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
"kebab-case-filename": {
|
|
92
|
+
meta: {
|
|
93
|
+
schema: []
|
|
94
|
+
},
|
|
95
|
+
create(context) {
|
|
96
|
+
return {
|
|
97
|
+
Program(node) {
|
|
98
|
+
const filename = context.filename;
|
|
99
|
+
if (filename.includes("node_modules") || filename.endsWith(".d.ts")) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const parts = filename.split("/");
|
|
103
|
+
const basename = parts.at(-1);
|
|
104
|
+
if (!basename) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (basename.includes("$") || basename.includes("[") || basename.includes("]")) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const nameWithoutExt = basename.replace(/\.(test|e2e|server|client|config)\.(ts|tsx|js|jsx|mjs|cjs)$/, "").replace(/\.(ts|tsx|js|jsx|mjs|cjs)$/, "");
|
|
111
|
+
if (nameWithoutExt === "") {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (nameWithoutExt === "routeTree.gen" || nameWithoutExt === "Dockerfile" || nameWithoutExt.startsWith("__") || nameWithoutExt.endsWith(".gen") || nameWithoutExt === "vite-env") {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const kebabRegex = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
118
|
+
if (!kebabRegex.test(nameWithoutExt)) {
|
|
119
|
+
context.report({
|
|
120
|
+
message: `Filename "${basename}" must use kebab-case (e.g., "my-component.tsx").`,
|
|
121
|
+
node
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
var max_file_lines_default = plugin;
|
|
131
|
+
export {
|
|
132
|
+
max_file_lines_default as default
|
|
133
|
+
};
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import type { Plugin } from "#oxlint/plugins";
|
|
2
|
+
|
|
3
|
+
function parseOptions(value: unknown): {
|
|
4
|
+
maxLines: number;
|
|
5
|
+
extensions: string[];
|
|
6
|
+
} {
|
|
7
|
+
if (typeof value !== "object" || value === null) {
|
|
8
|
+
return { maxLines: 500, extensions: [".tsx"] };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const maxLines =
|
|
12
|
+
typeof value.maxLines === "number"
|
|
13
|
+
? value.maxLines
|
|
14
|
+
: 500;
|
|
15
|
+
const extensions = Array.isArray(value.extensions)
|
|
16
|
+
? value.extensions.filter(
|
|
17
|
+
(extension) => typeof extension === "string"
|
|
18
|
+
)
|
|
19
|
+
: [".tsx"];
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
maxLines,
|
|
23
|
+
extensions:
|
|
24
|
+
extensions.length > 0 ? extensions : [".tsx"],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const plugin: Plugin = {
|
|
29
|
+
meta: {
|
|
30
|
+
name: "file-quality",
|
|
31
|
+
},
|
|
32
|
+
rules: {
|
|
33
|
+
"max-file-lines": {
|
|
34
|
+
meta: {
|
|
35
|
+
defaultOptions: [
|
|
36
|
+
{
|
|
37
|
+
maxLines: 500,
|
|
38
|
+
extensions: [".tsx"],
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
schema: [
|
|
42
|
+
{
|
|
43
|
+
type: "object",
|
|
44
|
+
default: {},
|
|
45
|
+
properties: {
|
|
46
|
+
maxLines: { type: "number", default: 500 },
|
|
47
|
+
extensions: {
|
|
48
|
+
type: "array",
|
|
49
|
+
items: { type: "string" },
|
|
50
|
+
default: [".tsx"],
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
additionalProperties: false,
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
create(context) {
|
|
58
|
+
return {
|
|
59
|
+
Program() {
|
|
60
|
+
const options = parseOptions(
|
|
61
|
+
context.options[0]
|
|
62
|
+
);
|
|
63
|
+
const { maxLines, extensions } = options;
|
|
64
|
+
|
|
65
|
+
if (
|
|
66
|
+
!extensions.some((ext) =>
|
|
67
|
+
context.filename.endsWith(ext)
|
|
68
|
+
)
|
|
69
|
+
) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const lineCount =
|
|
74
|
+
context.sourceCode.lines.length;
|
|
75
|
+
if (lineCount <= maxLines) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
context.report({
|
|
80
|
+
message: `File has ${lineCount} lines (max ${maxLines}). Consider splitting into smaller modules.`,
|
|
81
|
+
node: context.sourceCode.ast,
|
|
82
|
+
});
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
"no-barrel-files": {
|
|
88
|
+
meta: {
|
|
89
|
+
schema: [],
|
|
90
|
+
},
|
|
91
|
+
create(context) {
|
|
92
|
+
return {
|
|
93
|
+
Program(node) {
|
|
94
|
+
const filename = context.filename;
|
|
95
|
+
|
|
96
|
+
// Only check index.ts files (not .tsx — route files are OK)
|
|
97
|
+
if (!filename.endsWith("/index.ts")) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Exception: any index.ts inside packages/ (root and sub-path entry points)
|
|
102
|
+
if (/packages\/[^/]+\/src\//.test(filename)) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const body = node.body;
|
|
107
|
+
if (body.length === 0) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const isBarrel = body.every(
|
|
112
|
+
(stmt) =>
|
|
113
|
+
(stmt.type === "ExportNamedDeclaration" &&
|
|
114
|
+
stmt.source !== null) ||
|
|
115
|
+
stmt.type === "ExportAllDeclaration" ||
|
|
116
|
+
stmt.type === "ImportDeclaration"
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
if (isBarrel) {
|
|
120
|
+
context.report({
|
|
121
|
+
message:
|
|
122
|
+
"Barrel files (index.ts with only re-exports) are not allowed. Import directly from source files.",
|
|
123
|
+
node,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
"kebab-case-filename": {
|
|
131
|
+
meta: {
|
|
132
|
+
schema: [],
|
|
133
|
+
},
|
|
134
|
+
create(context) {
|
|
135
|
+
return {
|
|
136
|
+
Program(node) {
|
|
137
|
+
const filename = context.filename;
|
|
138
|
+
|
|
139
|
+
// Skip node_modules and .d.ts files
|
|
140
|
+
if (
|
|
141
|
+
filename.includes("node_modules") ||
|
|
142
|
+
filename.endsWith(".d.ts")
|
|
143
|
+
) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const parts = filename.split("/");
|
|
148
|
+
const basename = parts.at(-1);
|
|
149
|
+
if (!basename) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Skip TanStack Router dynamic segments ($param, $, [param])
|
|
154
|
+
if (
|
|
155
|
+
basename.includes("$") ||
|
|
156
|
+
basename.includes("[") ||
|
|
157
|
+
basename.includes("]")
|
|
158
|
+
) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Strip compound extensions first, then simple
|
|
163
|
+
const nameWithoutExt = basename
|
|
164
|
+
.replace(
|
|
165
|
+
/\.(test|e2e|server|client|config)\.(ts|tsx|js|jsx|mjs|cjs)$/,
|
|
166
|
+
""
|
|
167
|
+
)
|
|
168
|
+
.replace(/\.(ts|tsx|js|jsx|mjs|cjs)$/, "");
|
|
169
|
+
|
|
170
|
+
// Skip empty names (e.g., just an extension)
|
|
171
|
+
if (nameWithoutExt === "") {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Skip known special files
|
|
176
|
+
if (
|
|
177
|
+
nameWithoutExt === "routeTree.gen" ||
|
|
178
|
+
nameWithoutExt === "Dockerfile" ||
|
|
179
|
+
nameWithoutExt.startsWith("__") ||
|
|
180
|
+
nameWithoutExt.endsWith(".gen") ||
|
|
181
|
+
nameWithoutExt === "vite-env"
|
|
182
|
+
) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const kebabRegex =
|
|
187
|
+
/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
188
|
+
if (!kebabRegex.test(nameWithoutExt)) {
|
|
189
|
+
context.report({
|
|
190
|
+
message: `Filename "${basename}" must use kebab-case (e.g., "my-component.tsx").`,
|
|
191
|
+
node,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
export default plugin;
|