@blogic-cz/oxc-config 1.0.0 → 1.0.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/.oxlintrc.ts.json +26 -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",
|
|
@@ -52,7 +56,22 @@
|
|
|
52
56
|
"unicorn/throw-new-error": "error",
|
|
53
57
|
"vitest/no-import-node-test": "error",
|
|
54
58
|
"oxc/no-map-spread": "error",
|
|
55
|
-
"promise/always-return": "error"
|
|
59
|
+
"promise/always-return": "error",
|
|
60
|
+
"file-quality/kebab-case-filename": "error",
|
|
61
|
+
"file-quality/max-file-lines": [
|
|
62
|
+
"off",
|
|
63
|
+
{
|
|
64
|
+
"maxLines": 500,
|
|
65
|
+
"extensions": [".tsx"]
|
|
66
|
+
}
|
|
67
|
+
],
|
|
68
|
+
"file-quality/no-barrel-files": "error",
|
|
69
|
+
"naming/enforce-props-type-name": "error",
|
|
70
|
+
"naming/no-default-parameter-values": "error",
|
|
71
|
+
"naming/no-console-in-server": "error",
|
|
72
|
+
"naming/no-dynamic-type-import": "error",
|
|
73
|
+
"naming/no-server-logger-in-client": "error",
|
|
74
|
+
"naming/no-undefined-parameter-union": "error"
|
|
56
75
|
},
|
|
57
76
|
"overrides": [
|
|
58
77
|
{
|
|
@@ -90,6 +109,9 @@
|
|
|
90
109
|
"eslint/no-shadow": "off",
|
|
91
110
|
"eslint/require-await": "off",
|
|
92
111
|
"import/no-relative-parent-imports": "off",
|
|
112
|
+
"naming/enforce-props-type-name": "off",
|
|
113
|
+
"naming/no-default-parameter-values": "off",
|
|
114
|
+
"naming/no-undefined-parameter-union": "off",
|
|
93
115
|
"typescript/no-unsafe-type-assertion": "off",
|
|
94
116
|
"unicorn/no-array-sort": "off"
|
|
95
117
|
}
|
|
@@ -102,6 +124,7 @@
|
|
|
102
124
|
"rules": {
|
|
103
125
|
"eslint/no-new": "off",
|
|
104
126
|
"import/no-relative-parent-imports": "off",
|
|
127
|
+
"naming/no-default-parameter-values": "off",
|
|
105
128
|
"typescript/consistent-type-definitions": "off",
|
|
106
129
|
"typescript/no-import-type-side-effects": "off",
|
|
107
130
|
"typescript/no-non-null-assertion": "off"
|
|
@@ -121,7 +144,8 @@
|
|
|
121
144
|
"packages/logger/src/**"
|
|
122
145
|
],
|
|
123
146
|
"rules": {
|
|
124
|
-
"eslint/require-await": "off"
|
|
147
|
+
"eslint/require-await": "off",
|
|
148
|
+
"naming/no-default-parameter-values": "off"
|
|
125
149
|
}
|
|
126
150
|
}
|
|
127
151
|
]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blogic-cz/oxc-config",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
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;
|