@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 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.0.0",
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;