@yasainet/eslint 0.0.24 → 0.0.26

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.24",
3
+ "version": "0.0.26",
4
4
  "description": "ESLint",
5
5
  "type": "module",
6
6
  "exports": {
@@ -92,7 +92,7 @@ function prefixLibPatterns(prefix, mapping) {
92
92
 
93
93
  const LIB_BOUNDARY_PATTERNS = [
94
94
  {
95
- group: ["**/lib/*", "**/lib/**"],
95
+ group: ["@/lib/*", "@/lib/**"],
96
96
  message:
97
97
  "lib/* can only be imported from repositories (lib-boundary violation)",
98
98
  },
@@ -1,10 +1,12 @@
1
1
  import { actionHandleServiceRule } from "./action-handle-service.mjs";
2
2
  import { importPathStyleRule } from "./import-path-style.mjs";
3
+ import { namespaceImportNameRule } from "./namespace-import-name.mjs";
3
4
 
4
5
  /** Single plugin object to avoid ESLint "Cannot redefine plugin" errors. */
5
6
  export const localPlugin = {
6
7
  rules: {
7
8
  "action-handle-service": actionHandleServiceRule,
8
9
  "import-path-style": importPathStyleRule,
10
+ "namespace-import-name": namespaceImportNameRule,
9
11
  },
10
12
  };
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Enforce consistent naming for `import * as` namespace imports
3
+ * within feature-based architecture.
4
+ *
5
+ * Convention: import * as {featureName}{Scope}{Layer} from "{path}/{scope}.{layerExt}"
6
+ */
7
+
8
+ /** @type {Record<string, string>} */
9
+ const LAYER_MAP = {
10
+ repo: "Repository",
11
+ service: "Service",
12
+ domain: "Domain",
13
+ action: "Action",
14
+ util: "Util",
15
+ type: "Type",
16
+ schema: "Schema",
17
+ constant: "Constant",
18
+ };
19
+
20
+ /** Convert a snake_case or kebab-case string to camelCase. */
21
+ function toCamelCase(str) {
22
+ return str.replace(/[-_]+(.)/g, (_, c) => c.toUpperCase());
23
+ }
24
+
25
+ /** Convert a snake_case or kebab-case string to PascalCase. */
26
+ function toPascalCase(str) {
27
+ const camel = toCamelCase(str);
28
+ return camel.charAt(0).toUpperCase() + camel.slice(1);
29
+ }
30
+
31
+ /**
32
+ * Parse import source to extract featureName, scope, and layer.
33
+ * Returns null if the source doesn't match the expected pattern.
34
+ */
35
+ function parseImportSource(importPath, featureRoot) {
36
+ // Normalize alias: @/features/... → features/...
37
+ let normalized = importPath;
38
+ const aliasBase = featureRoot.replace(/^src\//, "@/");
39
+ if (normalized.startsWith(aliasBase + "/")) {
40
+ normalized = featureRoot + normalized.slice(aliasBase.length);
41
+ }
42
+
43
+ // Only process paths within the feature root
44
+ const rootPrefix = featureRoot + "/";
45
+ if (!normalized.startsWith(rootPrefix)) return null;
46
+
47
+ const afterRoot = normalized.slice(rootPrefix.length);
48
+ // Expected: {feature}/{layerDir}/{scope}.{layerExt}
49
+ const segments = afterRoot.split("/");
50
+ if (segments.length < 2) return null;
51
+
52
+ const featureDir = segments[0];
53
+ const fileName = segments[segments.length - 1];
54
+
55
+ const dotIdx = fileName.indexOf(".");
56
+ if (dotIdx === -1) return null;
57
+
58
+ const scope = fileName.slice(0, dotIdx);
59
+ const ext = fileName.slice(dotIdx + 1);
60
+
61
+ const layer = LAYER_MAP[ext];
62
+ if (!layer) return null;
63
+
64
+ return { featureDir, scope, layer };
65
+ }
66
+
67
+ /** Build the expected namespace import name. */
68
+ function buildExpectedName(featureDir, scope, layer) {
69
+ const featureCamel = toCamelCase(featureDir);
70
+ const scopePascal = toPascalCase(scope);
71
+
72
+ // Dedup: if feature is "shared" or featureName === scope, omit feature prefix
73
+ if (featureDir === "shared" || featureCamel === scope) {
74
+ return scope + layer;
75
+ }
76
+
77
+ return featureCamel + scopePascal + layer;
78
+ }
79
+
80
+ export const namespaceImportNameRule = {
81
+ meta: {
82
+ type: "suggestion",
83
+ messages: {
84
+ mismatch:
85
+ "Namespace import should be named '{{ expected }}' instead of '{{ actual }}'.",
86
+ },
87
+ schema: [
88
+ {
89
+ type: "object",
90
+ properties: {
91
+ featureRoot: { type: "string" },
92
+ },
93
+ required: ["featureRoot"],
94
+ additionalProperties: false,
95
+ },
96
+ ],
97
+ },
98
+ create(context) {
99
+ const { featureRoot } = context.options[0];
100
+
101
+ return {
102
+ ImportDeclaration(node) {
103
+ // Only check namespace imports: import * as Name
104
+ const nsSpecifier = node.specifiers.find(
105
+ (s) => s.type === "ImportNamespaceSpecifier",
106
+ );
107
+ if (!nsSpecifier) return;
108
+
109
+ const importPath = node.source.value;
110
+
111
+ // Skip external/built-in imports (only check relative and alias)
112
+ if (
113
+ !importPath.startsWith("./") &&
114
+ !importPath.startsWith("../") &&
115
+ !importPath.startsWith("@/")
116
+ ) {
117
+ return;
118
+ }
119
+
120
+ // For relative imports, resolve to an absolute-like path for parsing
121
+ let resolvedPath = importPath;
122
+ if (importPath.startsWith(".")) {
123
+ const fileDir = context.filename.replace(/\/[^/]+$/, "");
124
+ const parts = [...fileDir.split("/"), ...importPath.split("/")];
125
+ const resolved = [];
126
+ for (const p of parts) {
127
+ if (p === "..") resolved.pop();
128
+ else if (p !== ".") resolved.push(p);
129
+ }
130
+ resolvedPath = resolved.join("/");
131
+ }
132
+
133
+ const parsed = parseImportSource(resolvedPath, featureRoot);
134
+ if (!parsed) return;
135
+
136
+ const { featureDir, scope, layer } = parsed;
137
+ const expected = buildExpectedName(featureDir, scope, layer);
138
+ const actual = nsSpecifier.local.name;
139
+
140
+ if (actual !== expected) {
141
+ context.report({
142
+ node: nsSpecifier,
143
+ messageId: "mismatch",
144
+ data: { expected, actual },
145
+ });
146
+ }
147
+ },
148
+ };
149
+ },
150
+ };
@@ -4,10 +4,33 @@ import { checkFile } from "./plugins.mjs";
4
4
 
5
5
  /** Scope naming rules to the given feature root. */
6
6
  export function createNamingConfigs(featureRoot, prefixLibMapping) {
7
- const prefixPattern = `@(${Object.keys(prefixLibMapping).join("|")})`;
8
- const sharedPrefixPattern = `@(shared|${Object.keys(prefixLibMapping).join("|")})`;
7
+ const prefixes = Object.keys(prefixLibMapping);
8
+ const hasPrefixes = prefixes.length > 0;
9
+ const prefixPattern = hasPrefixes ? `@(${prefixes.join("|")})` : null;
10
+ const sharedPrefixPattern = hasPrefixes
11
+ ? `@(shared|${prefixes.join("|")})`
12
+ : "shared";
9
13
 
10
- const configs = [
14
+ const servicePattern = prefixPattern
15
+ ? `${prefixPattern}.service`
16
+ : "*.service";
17
+ const repoPattern = prefixPattern ? `${prefixPattern}.repo` : "*.repo";
18
+ const actionPattern = prefixPattern
19
+ ? `${prefixPattern}.action`
20
+ : "*.action";
21
+
22
+ const configs = [];
23
+
24
+ configs.push({
25
+ name: "naming/namespace-import-name",
26
+ files: featuresGlob(featureRoot, "**/*.ts"),
27
+ plugins: { local: localPlugin },
28
+ rules: {
29
+ "local/namespace-import-name": ["error", { featureRoot }],
30
+ },
31
+ });
32
+
33
+ configs.push(
11
34
  {
12
35
  name: "naming/services",
13
36
  files: featuresGlob(featureRoot, "**/services/*.ts"),
@@ -16,30 +39,33 @@ export function createNamingConfigs(featureRoot, prefixLibMapping) {
16
39
  rules: {
17
40
  "check-file/filename-naming-convention": [
18
41
  "error",
19
- { "**/*.ts": `${prefixPattern}.service` },
42
+ { "**/*.ts": servicePattern },
20
43
  ],
21
44
  },
22
45
  },
23
46
  {
24
- name: "naming/services-shared",
25
- files: featuresGlob(featureRoot, "shared/services/*.ts"),
47
+ name: "naming/repositories",
48
+ files: featuresGlob(featureRoot, "**/repositories/*.ts"),
49
+ ignores: featuresGlob(featureRoot, "shared/repositories/*.ts"),
26
50
  plugins: { "check-file": checkFile },
27
51
  rules: {
28
52
  "check-file/filename-naming-convention": [
29
53
  "error",
30
- { "**/*.ts": `${sharedPrefixPattern}.service` },
54
+ { "**/*.ts": repoPattern },
31
55
  ],
32
56
  },
33
57
  },
58
+ );
59
+
60
+ configs.push(
34
61
  {
35
- name: "naming/repositories",
36
- files: featuresGlob(featureRoot, "**/repositories/*.ts"),
37
- ignores: featuresGlob(featureRoot, "shared/repositories/*.ts"),
62
+ name: "naming/services-shared",
63
+ files: featuresGlob(featureRoot, "shared/services/*.ts"),
38
64
  plugins: { "check-file": checkFile },
39
65
  rules: {
40
66
  "check-file/filename-naming-convention": [
41
67
  "error",
42
- { "**/*.ts": `${prefixPattern}.repo` },
68
+ { "**/*.ts": `${sharedPrefixPattern}.service` },
43
69
  ],
44
70
  },
45
71
  },
@@ -54,7 +80,7 @@ export function createNamingConfigs(featureRoot, prefixLibMapping) {
54
80
  ],
55
81
  },
56
82
  },
57
- ];
83
+ );
58
84
 
59
85
  configs.push(
60
86
  {
@@ -153,18 +179,22 @@ export function createNamingConfigs(featureRoot, prefixLibMapping) {
153
179
  ],
154
180
  },
155
181
  },
156
- {
157
- name: "naming/actions",
158
- files: featuresGlob(featureRoot, "**/actions/*.ts"),
159
- ignores: featuresGlob(featureRoot, "shared/actions/*.ts"),
160
- plugins: { "check-file": checkFile },
161
- rules: {
162
- "check-file/filename-naming-convention": [
163
- "error",
164
- { "**/*.ts": `${prefixPattern}.action` },
165
- ],
166
- },
167
- },
182
+ );
183
+
184
+ configs.push({
185
+ name: "naming/actions",
186
+ files: featuresGlob(featureRoot, "**/actions/*.ts"),
187
+ ignores: featuresGlob(featureRoot, "shared/actions/*.ts"),
188
+ plugins: { "check-file": checkFile },
189
+ rules: {
190
+ "check-file/filename-naming-convention": [
191
+ "error",
192
+ { "**/*.ts": actionPattern },
193
+ ],
194
+ },
195
+ });
196
+
197
+ configs.push(
168
198
  {
169
199
  name: "naming/actions-shared",
170
200
  files: featuresGlob(featureRoot, "shared/actions/*.ts"),
@@ -0,0 +1,57 @@
1
+ const FUNCTIONS_ROOT = "supabase/functions";
2
+ const FEATURE_ROOT = "supabase/functions/_features";
3
+
4
+ /** Deno-specific import restriction rules. */
5
+ export const denoImportsConfigs = [
6
+ {
7
+ name: "deno/lib-boundary",
8
+ files: [`${FUNCTIONS_ROOT}/**/*.ts`],
9
+ ignores: [
10
+ `${FUNCTIONS_ROOT}/_lib/**`,
11
+ `${FEATURE_ROOT}/**/repositories/**`,
12
+ `${FEATURE_ROOT}/**/types/**`,
13
+ ],
14
+ rules: {
15
+ "no-restricted-imports": [
16
+ "error",
17
+ {
18
+ patterns: [
19
+ {
20
+ group: ["*/_lib/*", "*/_lib/**"],
21
+ message:
22
+ "_lib/ can only be imported from repositories (lib-boundary violation)",
23
+ },
24
+ ],
25
+ },
26
+ ],
27
+ },
28
+ },
29
+ {
30
+ name: "deno/commands-entry-point",
31
+ files: [`${FUNCTIONS_ROOT}/commands/**/*.ts`],
32
+ rules: {
33
+ "no-restricted-imports": [
34
+ "error",
35
+ {
36
+ patterns: [
37
+ {
38
+ group: ["**/services/*", "**/services"],
39
+ message:
40
+ "commands/ must not import services directly. Import from actions instead.",
41
+ },
42
+ {
43
+ group: ["**/repositories/*", "**/repositories"],
44
+ message:
45
+ "commands/ must not import repositories directly. Import from actions instead.",
46
+ },
47
+ {
48
+ group: ["*/_lib/*", "*/_lib/**"],
49
+ message:
50
+ "commands/ must not import _lib/ directly. Import from actions instead.",
51
+ },
52
+ ],
53
+ },
54
+ ],
55
+ },
56
+ },
57
+ ];
@@ -1,8 +1,10 @@
1
1
  import { createCommonConfigs } from "../common/index.mjs";
2
+ import { denoImportsConfigs } from "./imports.mjs";
3
+
4
+ const FEATURE_ROOT = "supabase/functions/_features";
2
5
 
3
6
  /** Deno ESLint flat config entry point. */
4
7
  export const eslintConfig = [
5
- ...createCommonConfigs("supabase/functions/features", {
6
- banAliasImports: true,
7
- }),
8
+ ...createCommonConfigs(FEATURE_ROOT, { banAliasImports: true }),
9
+ ...denoImportsConfigs,
8
10
  ];