@yasainet/eslint 0.0.52 → 0.0.54

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.52",
3
+ "version": "0.0.54",
4
4
  "description": "ESLint",
5
5
  "type": "module",
6
6
  "exports": {
@@ -3,20 +3,30 @@ import { createImportsConfigs } from "./imports.mjs";
3
3
  import { createJsdocConfigs } from "./jsdoc.mjs";
4
4
  import { createLayersConfigs } from "./layers.mjs";
5
5
  import { createLibNamingConfigs, createNamingConfigs, createUtilsNamingConfigs } from "./naming.mjs";
6
- import { rulesConfigs } from "./rules.mjs";
6
+ import { createRulesConfigs } from "./rules.mjs";
7
7
 
8
- /** Build common configs scoped to the given feature root. */
8
+ /**
9
+ * Build common configs scoped to the given feature root:
10
+ *
11
+ * - `banAliasImports: true` enforces relative imports inside the feature root
12
+ * - `typeAware: false` disables type-aware rules and `local/no-any-return`,
13
+ * for environments without a project tsconfig (e.g., Deno entry)
14
+ * - `rulesFiles` narrows the parser/rules block. Pass when combining multiple
15
+ * entries so each entry only overrides its own files (e.g., the deno entry
16
+ * passes `["supabase/functions/**\/*.ts"]` so it doesn't override next's
17
+ * parser settings on `src/`).
18
+ */
9
19
  export function createCommonConfigs(
10
20
  featureRoot,
11
- { banAliasImports = false } = {},
21
+ { banAliasImports = false, typeAware = true, rulesFiles } = {},
12
22
  ) {
13
23
  const prefixLibMapping = generatePrefixLibMapping(featureRoot);
14
24
  return [
15
- ...rulesConfigs,
25
+ ...createRulesConfigs({ typeAware, ...(rulesFiles && { files: rulesFiles }) }),
16
26
  ...createNamingConfigs(featureRoot, prefixLibMapping),
17
27
  ...createLibNamingConfigs(featureRoot),
18
28
  ...createUtilsNamingConfigs(featureRoot),
19
- ...createLayersConfigs(featureRoot),
29
+ ...createLayersConfigs(featureRoot, { typeAware }),
20
30
  ...createImportsConfigs(featureRoot, prefixLibMapping, { banAliasImports }),
21
31
  ...createJsdocConfigs(featureRoot),
22
32
  ];
@@ -1,11 +1,30 @@
1
1
  import { localPlugin } from "./local-plugins/index.mjs";
2
2
 
3
- /** Scope layer rules to the given feature root. */
4
- export function createLayersConfigs(featureRoot) {
3
+ /**
4
+ * Scope layer rules to the given feature root:
5
+ *
6
+ * - `typeAware: true` (default) includes `layers/no-any-return`, which uses
7
+ * the TypeScript checker to inspect inferred return types
8
+ * - `typeAware: false` skips it for environments where the checker cannot run
9
+ * (e.g., Deno files outside the project tsconfig)
10
+ */
11
+ export function createLayersConfigs(featureRoot, { typeAware = true } = {}) {
5
12
  const loggerSelector = "CallExpression[callee.object.name='logger']";
6
13
  const loggerMessage =
7
14
  "logger is not allowed outside interactors. Logging belongs in interactors.";
8
15
 
16
+ const noAnyReturnConfig = {
17
+ name: "layers/no-any-return",
18
+ files: [
19
+ `${featureRoot}/**/queries/*.ts`,
20
+ `${featureRoot}/**/services/*.ts`,
21
+ ],
22
+ plugins: { local: localPlugin },
23
+ rules: {
24
+ "local/no-any-return": "error",
25
+ },
26
+ };
27
+
9
28
  return [
10
29
  // Logger/console: all features except interactors
11
30
  {
@@ -74,17 +93,7 @@ export function createLayersConfigs(featureRoot) {
74
93
  // Boundary type safety: queries & services must not leak `any`
75
94
  // into their public API. Uses type-aware inspection of the inferred
76
95
  // return type so unannotated functions are still checked.
77
- {
78
- name: "layers/no-any-return",
79
- files: [
80
- `${featureRoot}/**/queries/*.ts`,
81
- `${featureRoot}/**/services/*.ts`,
82
- ],
83
- plugins: { local: localPlugin },
84
- rules: {
85
- "local/no-any-return": "error",
86
- },
87
- },
96
+ ...(typeAware ? [noAnyReturnConfig] : []),
88
97
  // Services: try-catch + logger + dead error fallbacks
89
98
  {
90
99
  name: "layers/services",
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Enforce {Verb}{Subject}FormState pattern for FormState type names.
3
+ *
4
+ * Targets TSInterfaceDeclaration and TSTypeAliasDeclaration whose name ends
5
+ * with "FormState". Requires at least two PascalCase words before FormState
6
+ * (e.g. SignInFormState, CreateCommentFormState) so the verb prefix is never
7
+ * omitted. Single-noun names like "ContactFormState" are forbidden — rename
8
+ * to "CreateContactFormState" to make intent explicit.
9
+ */
10
+
11
+ const FORM_STATE_ALLOW = /^[A-Z][a-z]+[A-Z]\w*FormState$/;
12
+
13
+ function reportIfInvalid(context, idNode) {
14
+ const name = idNode.name;
15
+ if (!name.endsWith("FormState")) return;
16
+ if (FORM_STATE_ALLOW.test(name)) return;
17
+ context.report({
18
+ node: idNode,
19
+ messageId: "invalidName",
20
+ data: { name },
21
+ });
22
+ }
23
+
24
+ export const formStateNamingRule = {
25
+ meta: {
26
+ type: "problem",
27
+ messages: {
28
+ invalidName:
29
+ "FormState type '{{ name }}' must follow {Verb}{Subject}FormState pattern (e.g. CreateCommentFormState, SignInFormState). At least two PascalCase words are required.",
30
+ },
31
+ schema: [],
32
+ },
33
+ create(context) {
34
+ return {
35
+ TSInterfaceDeclaration(node) {
36
+ if (node.id) reportIfInvalid(context, node.id);
37
+ },
38
+ TSTypeAliasDeclaration(node) {
39
+ if (node.id) reportIfInvalid(context, node.id);
40
+ },
41
+ };
42
+ },
43
+ };
@@ -1,18 +1,22 @@
1
1
  import { featureNameRule } from "./feature-name.mjs";
2
+ import { formStateNamingRule } from "./form-state-naming.mjs";
2
3
  import { importPathStyleRule } from "./import-path-style.mjs";
3
4
  import { namespaceImportNameRule } from "./namespace-import-name.mjs";
4
5
  import { noAnyReturnRule } from "./no-any-return.mjs";
5
6
  import { queriesExportRule } from "./queries-export.mjs";
7
+ import { queriesNamespaceImportRule } from "./queries-namespace-import.mjs";
6
8
  import { schemaNamingRule } from "./schema-naming.mjs";
7
9
 
8
10
  /** Single plugin object to avoid ESLint "Cannot redefine plugin" errors. */
9
11
  export const localPlugin = {
10
12
  rules: {
11
13
  "feature-name": featureNameRule,
14
+ "form-state-naming": formStateNamingRule,
12
15
  "import-path-style": importPathStyleRule,
13
16
  "namespace-import-name": namespaceImportNameRule,
14
17
  "no-any-return": noAnyReturnRule,
15
18
  "queries-export": queriesExportRule,
19
+ "queries-namespace-import": queriesNamespaceImportRule,
16
20
  "schema-naming": schemaNamingRule,
17
21
  },
18
22
  };
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Enforce namespace imports for `queries/*.query.ts` files.
3
+ *
4
+ * Value imports must use `import * as xxxQuery from "..."` so that the
5
+ * `naming/namespace-import-name` rule can guarantee a single canonical
6
+ * binding (e.g. `comicsServerQuery.getComics`). Type-only imports are
7
+ * exempted because they have no runtime presence.
8
+ */
9
+
10
+ const QUERIES_PATH = /\/queries\/[^/]+\.query$/;
11
+
12
+ export const queriesNamespaceImportRule = {
13
+ meta: {
14
+ type: "problem",
15
+ messages: {
16
+ useNamespace:
17
+ 'Use `import * as xxxQuery from "{{ source }}"` instead of named imports for queries layer. Type-only imports (`import type {}`) are allowed.',
18
+ },
19
+ schema: [],
20
+ },
21
+ create(context) {
22
+ return {
23
+ ImportDeclaration(node) {
24
+ if (typeof node.source.value !== "string") return;
25
+ if (!QUERIES_PATH.test(node.source.value)) return;
26
+ if (node.importKind === "type") return;
27
+
28
+ for (const specifier of node.specifiers) {
29
+ if (specifier.type !== "ImportSpecifier") continue;
30
+ if (specifier.importKind === "type") continue;
31
+ context.report({
32
+ node: specifier,
33
+ messageId: "useNamespace",
34
+ data: { source: node.source.value },
35
+ });
36
+ }
37
+ },
38
+ };
39
+ },
40
+ };
@@ -110,6 +110,22 @@ export function createNamingConfigs(featureRoot, prefixLibMapping) {
110
110
  "local/queries-export": "error",
111
111
  },
112
112
  },
113
+ {
114
+ name: "naming/queries-namespace-import",
115
+ files: featuresGlob(featureRoot, "**/*.ts"),
116
+ plugins: { local: localPlugin },
117
+ rules: {
118
+ "local/queries-namespace-import": "error",
119
+ },
120
+ },
121
+ {
122
+ name: "naming/form-state",
123
+ files: featuresGlob(featureRoot, "**/*.ts"),
124
+ plugins: { local: localPlugin },
125
+ rules: {
126
+ "local/form-state-naming": "error",
127
+ },
128
+ },
113
129
  );
114
130
 
115
131
  configs.push(
@@ -23,87 +23,116 @@ const findProjectRoot = (start) => {
23
23
 
24
24
  const projectRoot = findProjectRoot(import.meta.dirname);
25
25
 
26
- /** Base rule configs for code style and TypeScript checks. */
27
- export const rulesConfigs = [
28
- {
29
- name: "rules/shared",
30
- plugins: {
31
- "@stylistic": stylistic,
32
- "simple-import-sort": simpleImportSortPlugin,
33
- },
34
- rules: {
35
- "no-console": "warn",
36
- "no-irregular-whitespace": [
37
- "warn",
38
- {
39
- skipStrings: false,
40
- skipComments: false,
41
- skipRegExps: false,
42
- skipTemplates: false,
43
- },
44
- ],
45
- "simple-import-sort/imports": "warn",
46
- "simple-import-sort/exports": "warn",
47
- "@stylistic/quotes": ["warn", "double", { avoidEscape: true }],
48
- // Dead code detection: rules with no legitimate use case, so always safe to error.
49
- "no-unreachable": "error",
50
- "no-unreachable-loop": "error",
51
- "no-useless-return": "error",
52
- "no-constant-condition": "error",
53
- "no-constant-binary-expression": "error",
54
- "no-dupe-else-if": "error",
55
- "no-self-assign": "error",
56
- "no-self-compare": "error",
57
- "no-useless-catch": "error",
58
- "no-fallthrough": "error",
59
- },
26
+ const sharedRulesConfig = {
27
+ name: "rules/shared",
28
+ plugins: {
29
+ "@stylistic": stylistic,
30
+ "simple-import-sort": simpleImportSortPlugin,
60
31
  },
61
- {
62
- name: "rules/typescript",
63
- files: ["**/*.ts", "**/*.tsx"],
64
- languageOptions: {
65
- parser: tseslint.parser,
66
- // Enable type-aware linting so rules like `no-unnecessary-condition`
67
- // can consult the TypeScript type checker.
68
- parserOptions: {
69
- projectService: true,
70
- tsconfigRootDir: projectRoot,
32
+ rules: {
33
+ "no-console": "warn",
34
+ "no-irregular-whitespace": [
35
+ "warn",
36
+ {
37
+ skipStrings: false,
38
+ skipComments: false,
39
+ skipRegExps: false,
40
+ skipTemplates: false,
71
41
  },
42
+ ],
43
+ "simple-import-sort/imports": "warn",
44
+ "simple-import-sort/exports": "warn",
45
+ "@stylistic/quotes": ["warn", "double", { avoidEscape: true }],
46
+ // Dead code detection: rules with no legitimate use case, so always safe to error.
47
+ "no-unreachable": "error",
48
+ "no-unreachable-loop": "error",
49
+ "no-useless-return": "error",
50
+ "no-constant-condition": "error",
51
+ "no-constant-binary-expression": "error",
52
+ "no-dupe-else-if": "error",
53
+ "no-self-assign": "error",
54
+ "no-self-compare": "error",
55
+ "no-useless-catch": "error",
56
+ "no-fallthrough": "error",
57
+ },
58
+ };
59
+
60
+ const syntacticTypeScriptRules = {
61
+ "@typescript-eslint/no-unused-vars": [
62
+ "error",
63
+ {
64
+ argsIgnorePattern: "^_",
65
+ varsIgnorePattern: "^_",
66
+ caughtErrorsIgnorePattern: "^_",
72
67
  },
73
- plugins: {
74
- "@typescript-eslint": tseslint.plugin,
75
- },
76
- rules: {
77
- "@typescript-eslint/no-unused-vars": [
78
- "error",
79
- {
80
- argsIgnorePattern: "^_",
81
- varsIgnorePattern: "^_",
82
- caughtErrorsIgnorePattern: "^_",
83
- },
84
- ],
85
- "@typescript-eslint/consistent-type-imports": [
86
- "error",
87
- { prefer: "type-imports" },
88
- ],
89
- "@typescript-eslint/no-explicit-any": "warn",
90
- // Detect defensive fallbacks on non-nullable values (e.g., `?? ''`
91
- // on a non-null column). Kept at warn until existing violations are
92
- // cleaned up across consuming projects; promote to error afterwards.
93
- "@typescript-eslint/no-unnecessary-condition": "warn",
94
- // Type-aware async safety: silent await omissions are a leading cause
95
- // of race conditions in server actions and background tasks.
96
- "@typescript-eslint/no-floating-promises": "error",
97
- "@typescript-eslint/no-misused-promises": "error",
98
- "@typescript-eslint/await-thenable": "error",
99
- "@typescript-eslint/require-await": "error",
100
- // Type-aware `any` propagation checks: any が境界を越えた瞬間に
101
- // 残りのコードで型検査が無効化されるため、検出したら確実に止める。
102
- "@typescript-eslint/no-unsafe-assignment": "error",
103
- "@typescript-eslint/no-unsafe-call": "error",
104
- "@typescript-eslint/no-unsafe-member-access": "error",
105
- "@typescript-eslint/no-unsafe-argument": "error",
106
- "@typescript-eslint/no-unsafe-return": "error",
68
+ ],
69
+ "@typescript-eslint/consistent-type-imports": [
70
+ "error",
71
+ { prefer: "type-imports" },
72
+ ],
73
+ "@typescript-eslint/no-explicit-any": "warn",
74
+ };
75
+
76
+ const typeAwareTypeScriptRules = {
77
+ // Detect defensive fallbacks on non-nullable values (e.g., `?? ''`
78
+ // on a non-null column). Promoted to error once consuming projects
79
+ // (bitcomic.net, getpayme.net) reached 0 warnings.
80
+ "@typescript-eslint/no-unnecessary-condition": "error",
81
+ // Type-aware async safety: silent await omissions are a leading cause
82
+ // of race conditions in server actions and background tasks.
83
+ "@typescript-eslint/no-floating-promises": "error",
84
+ "@typescript-eslint/no-misused-promises": "error",
85
+ "@typescript-eslint/await-thenable": "error",
86
+ "@typescript-eslint/require-await": "error",
87
+ // Type-aware `any` propagation checks: any が境界を越えた瞬間に
88
+ // 残りのコードで型検査が無効化されるため、検出したら確実に止める。
89
+ "@typescript-eslint/no-unsafe-assignment": "error",
90
+ "@typescript-eslint/no-unsafe-call": "error",
91
+ "@typescript-eslint/no-unsafe-member-access": "error",
92
+ "@typescript-eslint/no-unsafe-argument": "error",
93
+ "@typescript-eslint/no-unsafe-return": "error",
94
+ };
95
+
96
+ const typeAwareRulesOff = Object.fromEntries(
97
+ Object.keys(typeAwareTypeScriptRules).map((rule) => [rule, "off"]),
98
+ );
99
+
100
+ /**
101
+ * Build base rule configs:
102
+ *
103
+ * - `typeAware: true` (default) enables `projectService` and type-aware rules
104
+ * (`no-unnecessary-condition`, `no-floating-promises`, `no-unsafe-*`, etc.)
105
+ * for the matched `files`
106
+ * - `typeAware: false` disables `projectService` and forces type-aware rules
107
+ * off for the matched `files`. Use for files outside the project tsconfig
108
+ * (e.g., Deno files in Supabase Edge Functions)
109
+ *
110
+ * `files` defaults to all TypeScript sources. When combining multiple entries
111
+ * (e.g., next + deno), pass a narrow pattern so the type-aware override only
112
+ * applies to its target files.
113
+ */
114
+ export function createRulesConfigs({
115
+ typeAware = true,
116
+ files = ["**/*.ts", "**/*.tsx"],
117
+ } = {}) {
118
+ return [
119
+ sharedRulesConfig,
120
+ {
121
+ name: "rules/typescript",
122
+ files,
123
+ languageOptions: {
124
+ parser: tseslint.parser,
125
+ parserOptions: typeAware
126
+ ? { projectService: true, tsconfigRootDir: projectRoot }
127
+ : { projectService: false, project: null },
128
+ },
129
+ plugins: {
130
+ "@typescript-eslint": tseslint.plugin,
131
+ },
132
+ rules: {
133
+ ...syntacticTypeScriptRules,
134
+ ...(typeAware ? typeAwareTypeScriptRules : typeAwareRulesOff),
135
+ },
107
136
  },
108
- },
109
- ];
137
+ ];
138
+ }
@@ -9,9 +9,21 @@ const denoEntryPointConfigs = createEntryPointConfigs(
9
9
  ["supabase/functions/_*/**"],
10
10
  );
11
11
 
12
+ // Deno files are not covered by the consumer's project tsconfig
13
+ // (Supabase functions live outside Next.js's tsconfig), so type-aware
14
+ // rules cannot consult the type checker. Disable them here and rely on
15
+ // `deno check` / `deno lint` for type-level guarantees.
16
+ //
17
+ // `rulesFiles` is narrowed so the override only affects Deno files. When this
18
+ // entry is combined with `next`/`node`, those entries keep their type-aware
19
+ // settings on their own files.
12
20
  /** Deno ESLint flat config entry point. */
13
21
  export const eslintConfig = [
14
- ...createCommonConfigs(FEATURE_ROOT, { banAliasImports: true }),
22
+ ...createCommonConfigs(FEATURE_ROOT, {
23
+ banAliasImports: true,
24
+ typeAware: false,
25
+ rulesFiles: ["supabase/functions/**/*.ts"],
26
+ }),
15
27
  ...denoImportsConfigs,
16
28
  ...denoEntryPointConfigs,
17
29
  ];