@yasainet/eslint 0.0.32 → 0.0.34

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yasainet/eslint",
3
- "version": "0.0.32",
3
+ "version": "0.0.34",
4
4
  "description": "ESLint",
5
5
  "type": "module",
6
6
  "exports": {
@@ -0,0 +1,133 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ /**
5
+ * Extract table names from Supabase generated types file.
6
+ * Looks for top-level keys under `Tables: {` inside the `public` schema.
7
+ * Uses brace counting to handle deeply nested type definitions.
8
+ */
9
+ function extractTableNames(supabaseTypePath) {
10
+ if (!fs.existsSync(supabaseTypePath)) {
11
+ return [];
12
+ }
13
+
14
+ const content = fs.readFileSync(supabaseTypePath, "utf-8");
15
+
16
+ // Find `public:` excluding `graphql_public:` via negative lookbehind
17
+ const publicMatch = /(?<!\w)public:\s*\{/.exec(content);
18
+ if (!publicMatch) {
19
+ return [];
20
+ }
21
+
22
+ const tablesLabel = "Tables:";
23
+ const tablesIdx = content.indexOf(tablesLabel, publicMatch.index);
24
+ if (tablesIdx === -1) {
25
+ return [];
26
+ }
27
+
28
+ // Find the opening brace of `Tables: {`
29
+ const braceStart = content.indexOf("{", tablesIdx + tablesLabel.length);
30
+ if (braceStart === -1) {
31
+ return [];
32
+ }
33
+
34
+ // Extract the Tables block using brace counting
35
+ let depth = 0;
36
+ let blockEnd = -1;
37
+ for (let i = braceStart; i < content.length; i++) {
38
+ if (content[i] === "{") depth++;
39
+ else if (content[i] === "}") depth--;
40
+ if (depth === 0) {
41
+ blockEnd = i;
42
+ break;
43
+ }
44
+ }
45
+ if (blockEnd === -1) {
46
+ return [];
47
+ }
48
+
49
+ // Extract top-level keys (depth 0) inside the Tables block
50
+ const tablesBlock = content.slice(braceStart + 1, blockEnd);
51
+ const names = [];
52
+ depth = 0;
53
+ const keyRegex = /(\w+)\s*:/g;
54
+ let match;
55
+ while ((match = keyRegex.exec(tablesBlock)) !== null) {
56
+ // Count braces before this match to determine depth
57
+ const preceding = tablesBlock.slice(0, match.index);
58
+ let d = 0;
59
+ for (const ch of preceding) {
60
+ if (ch === "{") d++;
61
+ else if (ch === "}") d--;
62
+ }
63
+ if (d === 0) {
64
+ names.push(match[1]);
65
+ }
66
+ }
67
+ return names;
68
+ }
69
+
70
+ /** Convert snake_case to kebab-case. */
71
+ function toKebab(name) {
72
+ return name.replace(/_/g, "-");
73
+ }
74
+
75
+ /**
76
+ * Enforce that feature directory names match allowed values:
77
+ * "shared", "auth", plus Supabase table names converted to kebab-case.
78
+ */
79
+ export const featureNameRule = {
80
+ meta: {
81
+ type: "problem",
82
+ messages: {
83
+ invalidFeatureName:
84
+ "Feature directory '{{ name }}' is not allowed. Allowed: {{ allowed }}.",
85
+ },
86
+ schema: [
87
+ {
88
+ type: "object",
89
+ properties: {
90
+ featureRoot: { type: "string" },
91
+ },
92
+ required: ["featureRoot"],
93
+ additionalProperties: false,
94
+ },
95
+ ],
96
+ },
97
+ create(context) {
98
+ const { featureRoot } = context.options[0];
99
+ const filename = context.filename;
100
+
101
+ const rootSep = featureRoot + "/";
102
+ const rootIdx = filename.indexOf(rootSep);
103
+ if (rootIdx === -1) return {};
104
+
105
+ const afterRoot = filename.slice(rootIdx + rootSep.length);
106
+ const featureName = afterRoot.split("/")[0];
107
+ if (!featureName) return {};
108
+
109
+ const projectRoot = filename.slice(0, rootIdx).replace(/\/src$/, "");
110
+ const supabaseTypePath = path.join(
111
+ projectRoot,
112
+ featureRoot.replace(/features$/, "lib/supabase/supabase.type.ts"),
113
+ );
114
+
115
+ const tableNames = extractTableNames(supabaseTypePath);
116
+ const allowedNames = ["shared", "auth", ...tableNames.map(toKebab)];
117
+
118
+ if (allowedNames.includes(featureName)) return {};
119
+
120
+ return {
121
+ Program(node) {
122
+ context.report({
123
+ node,
124
+ messageId: "invalidFeatureName",
125
+ data: {
126
+ name: featureName,
127
+ allowed: allowedNames.join(", "),
128
+ },
129
+ });
130
+ },
131
+ };
132
+ },
133
+ };
@@ -1,4 +1,5 @@
1
1
  import { actionHandleServiceRule } from "./action-handle-service.mjs";
2
+ import { featureNameRule } from "./feature-name.mjs";
2
3
  import { importPathStyleRule } from "./import-path-style.mjs";
3
4
  import { namespaceImportNameRule } from "./namespace-import-name.mjs";
4
5
 
@@ -6,6 +7,7 @@ import { namespaceImportNameRule } from "./namespace-import-name.mjs";
6
7
  export const localPlugin = {
7
8
  rules: {
8
9
  "action-handle-service": actionHandleServiceRule,
10
+ "feature-name": featureNameRule,
9
11
  "import-path-style": importPathStyleRule,
10
12
  "namespace-import-name": namespaceImportNameRule,
11
13
  },
@@ -59,6 +59,15 @@ export function createNamingConfigs(featureRoot, prefixLibMapping) {
59
59
 
60
60
  const configs = [];
61
61
 
62
+ configs.push({
63
+ name: "naming/feature-name",
64
+ files: featuresGlob(featureRoot, "**/*.ts"),
65
+ plugins: { local: localPlugin },
66
+ rules: {
67
+ "local/feature-name": ["error", { featureRoot }],
68
+ },
69
+ });
70
+
62
71
  configs.push({
63
72
  name: "naming/namespace-import-name",
64
73
  files: featuresGlob(featureRoot, "**/*.ts"),