@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 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.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;