@yasainet/eslint 0.0.51 → 0.0.53
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/README.md +1 -1
- package/package.json +1 -1
- package/src/common/local-plugins/form-state-naming.mjs +43 -0
- package/src/common/local-plugins/index.mjs +6 -0
- package/src/common/local-plugins/queries-export.mjs +65 -0
- package/src/common/local-plugins/queries-namespace-import.mjs +40 -0
- package/src/common/naming.mjs +24 -0
- package/src/common/rules.mjs +3 -3
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ Each entry point enforces a feature-based architecture with the following conven
|
|
|
35
35
|
├── shared/ # Cross-feature shared modules
|
|
36
36
|
├── ...
|
|
37
37
|
{libRoot}/ # *.lib.ts — library wrappers (e.g., supabase.lib.ts)
|
|
38
|
-
{utilsRoot}/ # *.util.ts — top-level utilities
|
|
38
|
+
{utilsRoot}/ # *.util.ts — top-level utilities (e.g., font.util.ts)
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
## Setup
|
package/package.json
CHANGED
|
@@ -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,16 +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";
|
|
6
|
+
import { queriesExportRule } from "./queries-export.mjs";
|
|
7
|
+
import { queriesNamespaceImportRule } from "./queries-namespace-import.mjs";
|
|
5
8
|
import { schemaNamingRule } from "./schema-naming.mjs";
|
|
6
9
|
|
|
7
10
|
/** Single plugin object to avoid ESLint "Cannot redefine plugin" errors. */
|
|
8
11
|
export const localPlugin = {
|
|
9
12
|
rules: {
|
|
10
13
|
"feature-name": featureNameRule,
|
|
14
|
+
"form-state-naming": formStateNamingRule,
|
|
11
15
|
"import-path-style": importPathStyleRule,
|
|
12
16
|
"namespace-import-name": namespaceImportNameRule,
|
|
13
17
|
"no-any-return": noAnyReturnRule,
|
|
18
|
+
"queries-export": queriesExportRule,
|
|
19
|
+
"queries-namespace-import": queriesNamespaceImportRule,
|
|
14
20
|
"schema-naming": schemaNamingRule,
|
|
15
21
|
},
|
|
16
22
|
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enforce verb allow list for `queries/*.query.ts` exports.
|
|
3
|
+
*
|
|
4
|
+
* The queries layer is the TS-idiomatic translation of Rails 5 actions
|
|
5
|
+
* (index/show -> get, create, update, destroy -> delete). Auth ceremonies
|
|
6
|
+
* (signUp / signIn / signOut) are admitted as industry-standard exceptions.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const QUERIES_ALLOW = /^(get|create|update|delete|signUp|signIn|signOut)([A-Z]|$)/;
|
|
10
|
+
|
|
11
|
+
function isFunctionLike(initNode) {
|
|
12
|
+
if (!initNode) return false;
|
|
13
|
+
const t = initNode.type;
|
|
14
|
+
if (t === "ArrowFunctionExpression" || t === "FunctionExpression") return true;
|
|
15
|
+
if (t === "CallExpression") {
|
|
16
|
+
return initNode.arguments.some(
|
|
17
|
+
(arg) =>
|
|
18
|
+
arg.type === "ArrowFunctionExpression" ||
|
|
19
|
+
arg.type === "FunctionExpression",
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function reportIfInvalid(context, idNode) {
|
|
26
|
+
if (!QUERIES_ALLOW.test(idNode.name)) {
|
|
27
|
+
context.report({
|
|
28
|
+
node: idNode,
|
|
29
|
+
messageId: "invalidName",
|
|
30
|
+
data: { name: idNode.name },
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const queriesExportRule = {
|
|
36
|
+
meta: {
|
|
37
|
+
type: "problem",
|
|
38
|
+
messages: {
|
|
39
|
+
invalidName:
|
|
40
|
+
"queries export '{{ name }}' must start with one of: get, create, update, delete, signUp, signIn, signOut. (Rails 5 actions translated to TS idiom)",
|
|
41
|
+
},
|
|
42
|
+
schema: [],
|
|
43
|
+
},
|
|
44
|
+
create(context) {
|
|
45
|
+
return {
|
|
46
|
+
ExportNamedDeclaration(node) {
|
|
47
|
+
if (!node.declaration) return;
|
|
48
|
+
const decl = node.declaration;
|
|
49
|
+
|
|
50
|
+
if (decl.type === "FunctionDeclaration" && decl.id) {
|
|
51
|
+
reportIfInvalid(context, decl.id);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (decl.type === "VariableDeclaration") {
|
|
56
|
+
for (const variator of decl.declarations) {
|
|
57
|
+
if (variator.id.type !== "Identifier") continue;
|
|
58
|
+
if (!isFunctionLike(variator.init)) continue;
|
|
59
|
+
reportIfInvalid(context, variator.id);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
};
|
|
@@ -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
|
+
};
|
package/src/common/naming.mjs
CHANGED
|
@@ -102,6 +102,30 @@ export function createNamingConfigs(featureRoot, prefixLibMapping) {
|
|
|
102
102
|
],
|
|
103
103
|
},
|
|
104
104
|
},
|
|
105
|
+
{
|
|
106
|
+
name: "naming/queries-export",
|
|
107
|
+
files: featuresGlob(featureRoot, "**/queries/*.query.ts"),
|
|
108
|
+
plugins: { local: localPlugin },
|
|
109
|
+
rules: {
|
|
110
|
+
"local/queries-export": "error",
|
|
111
|
+
},
|
|
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
|
+
},
|
|
105
129
|
);
|
|
106
130
|
|
|
107
131
|
configs.push(
|
package/src/common/rules.mjs
CHANGED
|
@@ -88,9 +88,9 @@ export const rulesConfigs = [
|
|
|
88
88
|
],
|
|
89
89
|
"@typescript-eslint/no-explicit-any": "warn",
|
|
90
90
|
// Detect defensive fallbacks on non-nullable values (e.g., `?? ''`
|
|
91
|
-
// on a non-null column).
|
|
92
|
-
//
|
|
93
|
-
"@typescript-eslint/no-unnecessary-condition": "
|
|
91
|
+
// on a non-null column). Promoted to error once consuming projects
|
|
92
|
+
// (bitcomic.net, getpayme.net) reached 0 warnings.
|
|
93
|
+
"@typescript-eslint/no-unnecessary-condition": "error",
|
|
94
94
|
// Type-aware async safety: silent await omissions are a leading cause
|
|
95
95
|
// of race conditions in server actions and background tasks.
|
|
96
96
|
"@typescript-eslint/no-floating-promises": "error",
|