@sarj/eslint-plugin 0.2.1 → 1.0.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.
Files changed (47) hide show
  1. package/README.md +132 -3
  2. package/dist/index.d.ts +12 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +98 -0
  5. package/dist/rules/enforce-file-structure.d.ts +4 -0
  6. package/dist/rules/enforce-file-structure.d.ts.map +1 -0
  7. package/dist/rules/enforce-file-structure.js +90 -0
  8. package/dist/rules/no-enum.d.ts +8 -0
  9. package/dist/rules/no-enum.d.ts.map +1 -0
  10. package/dist/rules/no-enum.js +29 -0
  11. package/dist/rules/no-enum.test.d.ts +2 -0
  12. package/dist/rules/no-enum.test.d.ts.map +1 -0
  13. package/dist/rules/no-enum.test.js +44 -0
  14. package/dist/rules/no-raw-env.d.ts +8 -0
  15. package/dist/rules/no-raw-env.d.ts.map +1 -0
  16. package/dist/rules/no-raw-env.js +33 -0
  17. package/dist/rules/no-raw-env.test.d.ts +2 -0
  18. package/dist/rules/no-raw-env.test.d.ts.map +1 -0
  19. package/dist/rules/no-raw-env.test.js +36 -0
  20. package/dist/rules/prefer-shadcn.d.ts +4 -0
  21. package/dist/rules/prefer-shadcn.d.ts.map +1 -0
  22. package/dist/rules/prefer-shadcn.js +50 -0
  23. package/dist/rules/prefer-shadcn.test.d.ts +2 -0
  24. package/dist/rules/prefer-shadcn.test.d.ts.map +1 -0
  25. package/dist/rules/prefer-shadcn.test.js +58 -0
  26. package/dist/rules/require-assert-never.d.ts +8 -0
  27. package/dist/rules/require-assert-never.d.ts.map +1 -0
  28. package/dist/rules/require-assert-never.js +53 -0
  29. package/dist/rules/require-assert-never.test.d.ts +2 -0
  30. package/dist/rules/require-assert-never.test.d.ts.map +1 -0
  31. package/dist/rules/require-assert-never.test.js +76 -0
  32. package/dist/rules/require-zod-form-validation.d.ts +8 -0
  33. package/dist/rules/require-zod-form-validation.d.ts.map +1 -0
  34. package/dist/rules/require-zod-form-validation.js +59 -0
  35. package/dist/rules/zod-naming-convention.d.ts +8 -0
  36. package/dist/rules/zod-naming-convention.d.ts.map +1 -0
  37. package/dist/rules/zod-naming-convention.js +53 -0
  38. package/dist/rules/zod-naming-convention.test.d.ts +2 -0
  39. package/dist/rules/zod-naming-convention.test.d.ts.map +1 -0
  40. package/dist/rules/zod-naming-convention.test.js +38 -0
  41. package/package.json +33 -11
  42. package/.claude/settings.local.json +0 -9
  43. package/lib/index.js +0 -19
  44. package/lib/rules/enforce-file-structure.js +0 -85
  45. package/lib/rules/require-assert-never.js +0 -46
  46. package/lib/rules/require-zod-form-validation.js +0 -47
  47. package/lib/rules/zod-naming-convention.js +0 -44
package/README.md CHANGED
@@ -1,9 +1,138 @@
1
- # sarj-eslint
1
+ # @sarj/eslint-plugin
2
2
 
3
- Collection of custom ESLint rules.
3
+ Custom ESLint rules collection for Sarj projects. Supports ESLint 9+ flat config format.
4
4
 
5
5
  ## Installation
6
6
 
7
7
  ```bash
8
- npm install sarj-eslint --save-dev
8
+ yarn add -D @sarj/eslint-plugin eslint
9
9
  ```
10
+
11
+ ## Usage (ESLint 9+ Flat Config)
12
+
13
+ ```js
14
+ // eslint.config.js
15
+ import sarj, { configs } from "@sarj/eslint-plugin";
16
+
17
+ export default [
18
+ // Use a preset config
19
+ ...configs.recommended,
20
+
21
+ // Or configure manually
22
+ {
23
+ plugins: {
24
+ "@sarj": sarj,
25
+ },
26
+ rules: {
27
+ "@sarj/zod-naming-convention": "warn",
28
+ "@sarj/require-assert-never": "error",
29
+ },
30
+ },
31
+ ];
32
+ ```
33
+
34
+ ## Available Configs
35
+
36
+ | Config | Description |
37
+ |--------|-------------|
38
+ | `configs.recommended` | Basic sarj rules for everyday use |
39
+ | `configs.strict` | All sarj rules at error level |
40
+ | `configs.style-guide` | Comprehensive rules (sarj + core ESLint) |
41
+
42
+ ## Rules
43
+
44
+ | Rule | Description | Recommended |
45
+ |------|-------------|-------------|
46
+ | `@sarj/zod-naming-convention` | Enforce Zod schemas to be named with a Z prefix | warn |
47
+ | `@sarj/require-assert-never` | Require assertNever in switch default cases | error |
48
+ | `@sarj/require-zod-form-validation` | Require Zod validation when parsing FormData | error |
49
+ | `@sarj/enforce-file-structure` | Enforce consistent file structure | warn |
50
+ | `@sarj/no-raw-env` | Disallow direct process.env access | - |
51
+ | `@sarj/prefer-shadcn` | Prefer shadcn/ui components over native HTML | - |
52
+ | `@sarj/no-enum` | Disallow TypeScript enums | - |
53
+
54
+ ## Rule Details
55
+
56
+ ### `zod-naming-convention`
57
+
58
+ Ensures Zod schemas are named with a Z prefix for consistency.
59
+
60
+ ```ts
61
+ // Bad
62
+ const userSchema = z.object({ name: z.string() });
63
+
64
+ // Good
65
+ const ZUserSchema = z.object({ name: z.string() });
66
+ ```
67
+
68
+ ### `require-assert-never`
69
+
70
+ Ensures switch statements with default cases use assertNever for exhaustive type checking.
71
+
72
+ ```ts
73
+ // Bad
74
+ switch (status) {
75
+ case "active":
76
+ return 1;
77
+ default:
78
+ return 0;
79
+ }
80
+
81
+ // Good
82
+ switch (status) {
83
+ case "active":
84
+ return 1;
85
+ default:
86
+ assertNever(status);
87
+ }
88
+ ```
89
+
90
+ ### `no-raw-env`
91
+
92
+ Disallows direct access to process.env. Use a Zod-validated env schema instead.
93
+
94
+ ```ts
95
+ // Bad
96
+ const apiKey = process.env.API_KEY;
97
+
98
+ // Good
99
+ const apiKey = env.API_KEY; // from validated schema
100
+ ```
101
+
102
+ ### `prefer-shadcn`
103
+
104
+ Encourages use of shadcn/ui components instead of native HTML elements.
105
+
106
+ ```tsx
107
+ // Bad
108
+ <button onClick={handleClick}>Submit</button>
109
+
110
+ // Good
111
+ <Button onClick={handleClick}>Submit</Button>
112
+ ```
113
+
114
+ ### `no-enum`
115
+
116
+ Discourages TypeScript enums in favor of union types or const objects.
117
+
118
+ ```ts
119
+ // Bad
120
+ enum Status {
121
+ Active,
122
+ Inactive,
123
+ }
124
+
125
+ // Good
126
+ type Status = "active" | "inactive";
127
+ // or
128
+ const Status = { ACTIVE: "active", INACTIVE: "inactive" } as const;
129
+ ```
130
+
131
+ ## Requirements
132
+
133
+ - ESLint >= 9.0.0
134
+ - Node.js >= 18.0.0
135
+
136
+ ## License
137
+
138
+ MIT
@@ -0,0 +1,12 @@
1
+ import type { ESLint, Linter, Rule } from "eslint";
2
+ /**
3
+ * @sarj/eslint-plugin
4
+ *
5
+ * Custom ESLint rules collection for Sarj projects.
6
+ * Supports ESLint 9+ flat config format.
7
+ */
8
+ declare const rules: Record<string, Rule.RuleModule>;
9
+ declare const plugin: ESLint.Plugin;
10
+ declare const configs: Record<string, Linter.Config[]>;
11
+ export { plugin as default, configs, rules };
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAUnD;;;;;GAKG;AAEH,QAAA,MAAM,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,UAAU,CAQ1C,CAAC;AAEF,QAAA,MAAM,MAAM,EAAE,MAAM,CAAC,MAMpB,CAAC;AAGF,QAAA,MAAM,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAsE5C,CAAC;AAEF,OAAO,EAAE,MAAM,IAAI,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,98 @@
1
+ import { zodNamingConvention } from "./rules/zod-naming-convention.js";
2
+ import { requireAssertNever } from "./rules/require-assert-never.js";
3
+ import { requireZodFormValidation } from "./rules/require-zod-form-validation.js";
4
+ import { enforceFileStructure } from "./rules/enforce-file-structure.js";
5
+ import { noRawEnv } from "./rules/no-raw-env.js";
6
+ import { preferShadcn } from "./rules/prefer-shadcn.js";
7
+ import { noEnum } from "./rules/no-enum.js";
8
+ /**
9
+ * @sarj/eslint-plugin
10
+ *
11
+ * Custom ESLint rules collection for Sarj projects.
12
+ * Supports ESLint 9+ flat config format.
13
+ */
14
+ const rules = {
15
+ "zod-naming-convention": zodNamingConvention,
16
+ "require-assert-never": requireAssertNever,
17
+ "require-zod-form-validation": requireZodFormValidation,
18
+ "enforce-file-structure": enforceFileStructure,
19
+ "no-raw-env": noRawEnv,
20
+ "prefer-shadcn": preferShadcn,
21
+ "no-enum": noEnum,
22
+ };
23
+ const plugin = {
24
+ meta: {
25
+ name: "@sarj/eslint-plugin",
26
+ version: "1.0.0",
27
+ },
28
+ rules,
29
+ };
30
+ // ESLint 9 flat configs
31
+ const configs = {
32
+ // Basic sarj rules only
33
+ recommended: [
34
+ {
35
+ plugins: {
36
+ "@sarj": plugin,
37
+ },
38
+ rules: {
39
+ "@sarj/zod-naming-convention": "warn",
40
+ "@sarj/require-assert-never": "error",
41
+ "@sarj/require-zod-form-validation": "error",
42
+ "@sarj/enforce-file-structure": "warn",
43
+ },
44
+ },
45
+ ],
46
+ // All sarj rules at error level
47
+ strict: [
48
+ {
49
+ plugins: {
50
+ "@sarj": plugin,
51
+ },
52
+ rules: {
53
+ "@sarj/zod-naming-convention": "error",
54
+ "@sarj/require-assert-never": "error",
55
+ "@sarj/require-zod-form-validation": "error",
56
+ "@sarj/enforce-file-structure": "error",
57
+ "@sarj/no-raw-env": "error",
58
+ "@sarj/prefer-shadcn": "error",
59
+ "@sarj/no-enum": "error",
60
+ },
61
+ },
62
+ ],
63
+ // Comprehensive style guide rules (sarj + external)
64
+ "style-guide": [
65
+ {
66
+ plugins: {
67
+ "@sarj": plugin,
68
+ },
69
+ rules: {
70
+ // === Sarj custom rules ===
71
+ "@sarj/zod-naming-convention": "error",
72
+ "@sarj/require-assert-never": "error",
73
+ "@sarj/require-zod-form-validation": "error",
74
+ "@sarj/enforce-file-structure": "error",
75
+ "@sarj/no-raw-env": "error",
76
+ "@sarj/prefer-shadcn": "error",
77
+ "@sarj/no-enum": "error",
78
+ // === Core ESLint rules ===
79
+ "no-var": "error",
80
+ "prefer-const": "error",
81
+ "object-shorthand": ["error", "always"],
82
+ "no-nested-ternary": "error",
83
+ "no-console": ["warn", { allow: ["warn", "error"] }],
84
+ eqeqeq: ["error", "always"],
85
+ "no-param-reassign": "error",
86
+ "no-return-await": "error",
87
+ "prefer-template": "error",
88
+ "array-callback-return": "error",
89
+ // === Complexity limits ===
90
+ complexity: ["warn", { max: 15 }],
91
+ "max-depth": ["warn", { max: 4 }],
92
+ "max-nested-callbacks": ["warn", { max: 3 }],
93
+ "max-params": ["error", { max: 5 }],
94
+ },
95
+ },
96
+ ],
97
+ };
98
+ export { plugin as default, configs, rules };
@@ -0,0 +1,4 @@
1
+ import type { Rule } from "eslint";
2
+ export declare const enforceFileStructure: Rule.RuleModule;
3
+ export default enforceFileStructure;
4
+ //# sourceMappingURL=enforce-file-structure.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enforce-file-structure.d.ts","sourceRoot":"","sources":["../../src/rules/enforce-file-structure.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAgDnC,eAAO,MAAM,oBAAoB,EAAE,IAAI,CAAC,UA8DvC,CAAC;AAEF,eAAe,oBAAoB,CAAC"}
@@ -0,0 +1,90 @@
1
+ /**
2
+ * @fileoverview Enforce consistent file structure
3
+ * @description Enforces: imports -> types -> constants -> functions -> exports
4
+ */
5
+ const SECTIONS = ["imports", "types", "constants", "functions", "exports"];
6
+ function getStatementSection(statement) {
7
+ const type = statement.type;
8
+ switch (type) {
9
+ case "ImportDeclaration":
10
+ return 0; // imports
11
+ case "TSTypeAliasDeclaration":
12
+ case "TSInterfaceDeclaration":
13
+ case "TSEnumDeclaration":
14
+ return 1; // types
15
+ case "VariableDeclaration": {
16
+ // Check if it's a constant (const with UPPER_CASE or simple values)
17
+ const varDecl = statement;
18
+ if (varDecl.kind === "const") {
19
+ const declarator = varDecl.declarations[0];
20
+ if (declarator?.id.type === "Identifier" &&
21
+ declarator.id.name === declarator.id.name.toUpperCase()) {
22
+ return 2; // constants
23
+ }
24
+ }
25
+ return 3; // functions (const fn = ...)
26
+ }
27
+ case "FunctionDeclaration":
28
+ return 3; // functions
29
+ case "ExportNamedDeclaration":
30
+ case "ExportDefaultDeclaration":
31
+ return 4; // exports
32
+ default:
33
+ return 3; // default to functions
34
+ }
35
+ }
36
+ export const enforceFileStructure = {
37
+ meta: {
38
+ type: "suggestion",
39
+ docs: {
40
+ description: "Enforce consistent file structure: imports -> types -> constants -> functions -> exports",
41
+ recommended: true,
42
+ },
43
+ schema: [],
44
+ messages: {
45
+ incorrectOrder: "File structure violation: {{current}} should come after {{expected}}",
46
+ useServerDirective: "Server action files must start with 'use server' directive",
47
+ },
48
+ },
49
+ create(context) {
50
+ const filename = context.filename;
51
+ const isServerAction = filename.includes("/actions/") || filename.includes("action");
52
+ return {
53
+ Program(node) {
54
+ const program = node;
55
+ const body = program.body;
56
+ let currentSection = 0;
57
+ // Check for "use server" directive in server action files
58
+ if (isServerAction) {
59
+ const firstNode = body[0];
60
+ if (!firstNode ||
61
+ firstNode.type !== "ExpressionStatement" ||
62
+ firstNode.expression.type !== "Literal" ||
63
+ firstNode.expression.value !== "use server") {
64
+ context.report({
65
+ node,
66
+ messageId: "useServerDirective",
67
+ });
68
+ }
69
+ }
70
+ for (const statement of body) {
71
+ const statementSection = getStatementSection(statement);
72
+ if (statementSection < currentSection) {
73
+ context.report({
74
+ node: statement,
75
+ messageId: "incorrectOrder",
76
+ data: {
77
+ current: SECTIONS[statementSection],
78
+ expected: SECTIONS[currentSection],
79
+ },
80
+ });
81
+ }
82
+ else {
83
+ currentSection = Math.max(currentSection, statementSection);
84
+ }
85
+ }
86
+ },
87
+ };
88
+ },
89
+ };
90
+ export default enforceFileStructure;
@@ -0,0 +1,8 @@
1
+ import type { Rule } from "eslint";
2
+ /**
3
+ * @fileoverview Disallow TypeScript enums, prefer union types
4
+ * @description Enums add runtime code and have unintuitive behavior. Use union types instead.
5
+ */
6
+ export declare const noEnum: Rule.RuleModule;
7
+ export default noEnum;
8
+ //# sourceMappingURL=no-enum.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-enum.d.ts","sourceRoot":"","sources":["../../src/rules/no-enum.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAEnC;;;GAGG;AAEH,eAAO,MAAM,MAAM,EAAE,IAAI,CAAC,UAyBzB,CAAC;AAEF,eAAe,MAAM,CAAC"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @fileoverview Disallow TypeScript enums, prefer union types
3
+ * @description Enums add runtime code and have unintuitive behavior. Use union types instead.
4
+ */
5
+ export const noEnum = {
6
+ meta: {
7
+ type: "suggestion",
8
+ docs: {
9
+ description: "Disallow TypeScript enums, prefer union types or const objects",
10
+ recommended: true,
11
+ },
12
+ schema: [],
13
+ messages: {
14
+ noEnum: 'Enums are discouraged. Use union types (\'type Status = "active" | "inactive"\') or const objects instead.',
15
+ },
16
+ },
17
+ create(context) {
18
+ return {
19
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
20
+ TSEnumDeclaration(node) {
21
+ context.report({
22
+ node,
23
+ messageId: "noEnum",
24
+ });
25
+ },
26
+ };
27
+ },
28
+ };
29
+ export default noEnum;
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=no-enum.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-enum.test.d.ts","sourceRoot":"","sources":["../../src/rules/no-enum.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,44 @@
1
+ import { RuleTester } from "eslint";
2
+ import { describe, it } from "vitest";
3
+ import { noEnum } from "./no-enum.js";
4
+ // Note: This test requires @typescript-eslint/parser to work properly
5
+ // For now, we'll skip the actual test execution and just verify the rule exports correctly
6
+ describe("no-enum", () => {
7
+ it("should export a valid rule module", () => {
8
+ if (!noEnum.meta || !noEnum.create) {
9
+ throw new Error("Rule is not properly exported");
10
+ }
11
+ if (noEnum.meta.type !== "suggestion") {
12
+ throw new Error("Expected rule type to be 'suggestion'");
13
+ }
14
+ if (!noEnum.meta.messages?.noEnum) {
15
+ throw new Error("Expected noEnum message to be defined");
16
+ }
17
+ });
18
+ // Full test requires TypeScript parser
19
+ it("should work with TypeScript parser", async () => {
20
+ const parser = await import("@typescript-eslint/parser");
21
+ const ruleTester = new RuleTester({
22
+ languageOptions: {
23
+ ecmaVersion: 2022,
24
+ sourceType: "module",
25
+ parser,
26
+ },
27
+ });
28
+ ruleTester.run("no-enum", noEnum, {
29
+ valid: [
30
+ // Union types are OK
31
+ { code: 'type Status = "active" | "inactive";' },
32
+ // Const objects are OK
33
+ { code: 'const Status = { ACTIVE: "active", INACTIVE: "inactive" } as const;' },
34
+ ],
35
+ invalid: [
36
+ // Regular enum
37
+ {
38
+ code: "enum Status { Active, Inactive }",
39
+ errors: [{ messageId: "noEnum" }],
40
+ },
41
+ ],
42
+ });
43
+ });
44
+ });
@@ -0,0 +1,8 @@
1
+ import type { Rule } from "eslint";
2
+ /**
3
+ * @fileoverview Disallow direct process.env access
4
+ * @description Use Zod-validated env schema instead of process.env
5
+ */
6
+ export declare const noRawEnv: Rule.RuleModule;
7
+ export default noRawEnv;
8
+ //# sourceMappingURL=no-raw-env.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-raw-env.d.ts","sourceRoot":"","sources":["../../src/rules/no-raw-env.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAGnC;;;GAGG;AAEH,eAAO,MAAM,QAAQ,EAAE,IAAI,CAAC,UA8B3B,CAAC;AAEF,eAAe,QAAQ,CAAC"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * @fileoverview Disallow direct process.env access
3
+ * @description Use Zod-validated env schema instead of process.env
4
+ */
5
+ export const noRawEnv = {
6
+ meta: {
7
+ type: "problem",
8
+ docs: {
9
+ description: "Disallow direct process.env access",
10
+ recommended: true,
11
+ },
12
+ schema: [],
13
+ messages: {
14
+ noRawEnv: "Use Zod-validated env schema instead of process.env directly",
15
+ },
16
+ },
17
+ create(context) {
18
+ return {
19
+ MemberExpression(node) {
20
+ if (node.object.type === "Identifier" &&
21
+ node.object.name === "process" &&
22
+ node.property.type === "Identifier" &&
23
+ node.property.name === "env") {
24
+ context.report({
25
+ node,
26
+ messageId: "noRawEnv",
27
+ });
28
+ }
29
+ },
30
+ };
31
+ },
32
+ };
33
+ export default noRawEnv;
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=no-raw-env.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-raw-env.test.d.ts","sourceRoot":"","sources":["../../src/rules/no-raw-env.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,36 @@
1
+ import { RuleTester } from "eslint";
2
+ import { describe, it } from "vitest";
3
+ import { noRawEnv } from "./no-raw-env.js";
4
+ const ruleTester = new RuleTester({
5
+ languageOptions: {
6
+ ecmaVersion: 2022,
7
+ sourceType: "module",
8
+ },
9
+ });
10
+ describe("no-raw-env", () => {
11
+ it("should pass valid and fail invalid cases", () => {
12
+ ruleTester.run("no-raw-env", noRawEnv, {
13
+ valid: [
14
+ // Using validated env schema
15
+ { code: 'const apiKey = env.API_KEY;' },
16
+ { code: 'const dbUrl = config.databaseUrl;' },
17
+ { code: 'const { NODE_ENV } = validatedEnv;' },
18
+ ],
19
+ invalid: [
20
+ // Direct process.env access
21
+ {
22
+ code: 'const apiKey = process.env.API_KEY;',
23
+ errors: [{ messageId: "noRawEnv" }],
24
+ },
25
+ {
26
+ code: 'if (process.env.NODE_ENV === "production") {}',
27
+ errors: [{ messageId: "noRawEnv" }],
28
+ },
29
+ {
30
+ code: 'console.log(process.env.DEBUG);',
31
+ errors: [{ messageId: "noRawEnv" }],
32
+ },
33
+ ],
34
+ });
35
+ });
36
+ });
@@ -0,0 +1,4 @@
1
+ import type { Rule } from "eslint";
2
+ export declare const preferShadcn: Rule.RuleModule;
3
+ export default preferShadcn;
4
+ //# sourceMappingURL=prefer-shadcn.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prefer-shadcn.d.ts","sourceRoot":"","sources":["../../src/rules/prefer-shadcn.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAgBnC,eAAO,MAAM,YAAY,EAAE,IAAI,CAAC,UAwC/B,CAAC;AAEF,eAAe,YAAY,CAAC"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * @fileoverview Prefer shadcn/ui components over native HTML elements
3
+ * @description Use shadcn components for consistent design system
4
+ */
5
+ const REPLACEMENTS = {
6
+ button: "Button",
7
+ input: "Input",
8
+ select: "Select",
9
+ textarea: "Textarea",
10
+ table: "Table",
11
+ dialog: "Dialog",
12
+ };
13
+ export const preferShadcn = {
14
+ meta: {
15
+ type: "suggestion",
16
+ docs: {
17
+ description: "Prefer shadcn/ui components over native HTML elements",
18
+ recommended: true,
19
+ },
20
+ schema: [],
21
+ messages: {
22
+ preferShadcn: "Use shadcn <{{replacement}}> component from @/components/ui/{{lowercase}} instead of native <{{element}}>",
23
+ },
24
+ },
25
+ create(context) {
26
+ return {
27
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
28
+ JSXOpeningElement(node) {
29
+ // Only check lowercase element names (native HTML)
30
+ if (!node.name || node.name.type !== "JSXIdentifier") {
31
+ return;
32
+ }
33
+ const elementName = node.name.name;
34
+ const replacement = REPLACEMENTS[elementName];
35
+ if (replacement) {
36
+ context.report({
37
+ node,
38
+ messageId: "preferShadcn",
39
+ data: {
40
+ element: elementName,
41
+ replacement,
42
+ lowercase: elementName,
43
+ },
44
+ });
45
+ }
46
+ },
47
+ };
48
+ },
49
+ };
50
+ export default preferShadcn;
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=prefer-shadcn.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prefer-shadcn.test.d.ts","sourceRoot":"","sources":["../../src/rules/prefer-shadcn.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,58 @@
1
+ import { RuleTester } from "eslint";
2
+ import { describe, it } from "vitest";
3
+ import { preferShadcn } from "./prefer-shadcn.js";
4
+ const ruleTester = new RuleTester({
5
+ languageOptions: {
6
+ ecmaVersion: 2022,
7
+ sourceType: "module",
8
+ parserOptions: {
9
+ ecmaFeatures: {
10
+ jsx: true,
11
+ },
12
+ },
13
+ },
14
+ });
15
+ describe("prefer-shadcn", () => {
16
+ it("should pass valid and fail invalid cases", () => {
17
+ ruleTester.run("prefer-shadcn", preferShadcn, {
18
+ valid: [
19
+ // shadcn components are OK
20
+ { code: '<Button onClick={handleClick}>Click me</Button>' },
21
+ { code: '<Input type="text" placeholder="Enter name" />' },
22
+ { code: '<Select value={value} onValueChange={setValue} />' },
23
+ { code: '<Table><TableBody /></Table>' },
24
+ // Other elements are OK
25
+ { code: '<div className="container">Content</div>' },
26
+ { code: '<span>Text</span>' },
27
+ { code: '<form onSubmit={handleSubmit}></form>' },
28
+ ],
29
+ invalid: [
30
+ // Native button
31
+ {
32
+ code: '<button onClick={handleClick}>Click me</button>',
33
+ errors: [{ messageId: "preferShadcn" }],
34
+ },
35
+ // Native input
36
+ {
37
+ code: '<input type="text" placeholder="Enter name" />',
38
+ errors: [{ messageId: "preferShadcn" }],
39
+ },
40
+ // Native select
41
+ {
42
+ code: '<select value={value} onChange={handleChange}><option>A</option></select>',
43
+ errors: [{ messageId: "preferShadcn" }],
44
+ },
45
+ // Native textarea
46
+ {
47
+ code: '<textarea rows={5} />',
48
+ errors: [{ messageId: "preferShadcn" }],
49
+ },
50
+ // Native table
51
+ {
52
+ code: '<table><tbody /></table>',
53
+ errors: [{ messageId: "preferShadcn" }],
54
+ },
55
+ ],
56
+ });
57
+ });
58
+ });