@yasainet/eslint 0.0.68 → 0.0.70
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
package/src/common/imports.mjs
CHANGED
|
@@ -56,6 +56,16 @@ const LATERAL_PATTERNS = {
|
|
|
56
56
|
message:
|
|
57
57
|
"entries cannot import other feature's entries (lateral violation)",
|
|
58
58
|
},
|
|
59
|
+
{
|
|
60
|
+
group: [
|
|
61
|
+
"@/features/*/services/*",
|
|
62
|
+
"@/features/*/services",
|
|
63
|
+
"!@/features/shared/services/*",
|
|
64
|
+
"!@/features/shared/services",
|
|
65
|
+
],
|
|
66
|
+
message:
|
|
67
|
+
"entries cannot import other feature's services. Use the same feature's service (1:1) or move orchestration into the service layer. `shared/services/*` is exempt for cross-cutting side effects (notifications etc.).",
|
|
68
|
+
},
|
|
59
69
|
],
|
|
60
70
|
};
|
|
61
71
|
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enforce 1:1 entry-to-service mapping for `**\/entries/*.ts` exports.
|
|
3
|
+
*
|
|
4
|
+
* Why: services are the orchestration layer (they may combine multiple queries
|
|
5
|
+
* and other features' queries). entries should be a thin wrapper that calls a
|
|
6
|
+
* single service function and normalizes the return shape into
|
|
7
|
+
* `{ data, error }`. If an entry calls more than one service, orchestration is
|
|
8
|
+
* leaking up into the entry layer.
|
|
9
|
+
*
|
|
10
|
+
* Detection rule:
|
|
11
|
+
*
|
|
12
|
+
* - For every exported async `FunctionDeclaration` in an entries file, count
|
|
13
|
+
* `CallExpression`s whose callee is a `MemberExpression` of the form
|
|
14
|
+
* `<binding>.<method>(...)` where `<binding>` matches the namespace import
|
|
15
|
+
* naming convention `*Service` (e.g. `articlesServerService`,
|
|
16
|
+
* `usersClientService`).
|
|
17
|
+
* - More than one such call inside the same exported function is an error.
|
|
18
|
+
*
|
|
19
|
+
* Exception (C-3):
|
|
20
|
+
*
|
|
21
|
+
* - Bindings starting with `shared` (e.g. `sharedDiscordService`,
|
|
22
|
+
* `sharedResendService`) are EXCLUDED from the count. These represent
|
|
23
|
+
* cross-cutting side-effect abstractions (Discord / Resend / Slack
|
|
24
|
+
* notifications) that don't fit the entry-service 1:1 model and are allowed
|
|
25
|
+
* to be invoked from entries directly.
|
|
26
|
+
*
|
|
27
|
+
* The rule reports the 2nd and later violations (the 1st call is permitted),
|
|
28
|
+
* so the fix surface is the redundant calls.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const SERVICE_BINDING_REGEX = /Service$/;
|
|
32
|
+
|
|
33
|
+
function isServiceCall(node) {
|
|
34
|
+
if (node?.type !== "CallExpression") return false;
|
|
35
|
+
const callee = node.callee;
|
|
36
|
+
if (callee.type !== "MemberExpression") return false;
|
|
37
|
+
const obj = callee.object;
|
|
38
|
+
if (obj.type !== "Identifier") return false;
|
|
39
|
+
if (!SERVICE_BINDING_REGEX.test(obj.name)) return false;
|
|
40
|
+
if (obj.name.startsWith("shared")) return false;
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function collectServiceCalls(root) {
|
|
45
|
+
const calls = [];
|
|
46
|
+
function visit(node) {
|
|
47
|
+
if (!node || typeof node !== "object") return;
|
|
48
|
+
if (isServiceCall(node)) calls.push(node);
|
|
49
|
+
for (const key of Object.keys(node)) {
|
|
50
|
+
if (key === "parent" || key === "loc" || key === "range") continue;
|
|
51
|
+
const value = node[key];
|
|
52
|
+
if (Array.isArray(value)) {
|
|
53
|
+
for (const item of value) visit(item);
|
|
54
|
+
} else if (value && typeof value === "object" && "type" in value) {
|
|
55
|
+
visit(value);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
visit(root);
|
|
60
|
+
return calls;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const entrySingleServiceCallRule = {
|
|
64
|
+
meta: {
|
|
65
|
+
type: "problem",
|
|
66
|
+
docs: {
|
|
67
|
+
description:
|
|
68
|
+
"Enforce entries call at most one (non-shared) feature service per exported function.",
|
|
69
|
+
},
|
|
70
|
+
messages: {
|
|
71
|
+
multipleServiceCalls:
|
|
72
|
+
"entry '{{ funcName }}' calls more than one feature service ({{ count }} total). entries must be a thin wrapper that calls a single service. Move orchestration into the service layer. `shared/services/*` (e.g. `sharedDiscordService`) is exempt.",
|
|
73
|
+
},
|
|
74
|
+
schema: [],
|
|
75
|
+
},
|
|
76
|
+
create(context) {
|
|
77
|
+
return {
|
|
78
|
+
ExportNamedDeclaration(node) {
|
|
79
|
+
if (!node.declaration) return;
|
|
80
|
+
const decl = node.declaration;
|
|
81
|
+
if (decl.type !== "FunctionDeclaration") return;
|
|
82
|
+
if (!decl.async) return;
|
|
83
|
+
if (!decl.id) return;
|
|
84
|
+
const funcName = decl.id.name;
|
|
85
|
+
const calls = collectServiceCalls(decl.body);
|
|
86
|
+
if (calls.length <= 1) return;
|
|
87
|
+
for (let i = 1; i < calls.length; i++) {
|
|
88
|
+
context.report({
|
|
89
|
+
node: calls[i],
|
|
90
|
+
messageId: "multipleServiceCalls",
|
|
91
|
+
data: { funcName, count: String(calls.length) },
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { entrySingleServiceCallRule } from "./entry-single-service-call.mjs";
|
|
1
2
|
import { entryTemplateRule } from "./entry-template.mjs";
|
|
2
3
|
import { featureNameRule } from "./feature-name.mjs";
|
|
3
4
|
import { formStateNamingRule } from "./form-state-naming.mjs";
|
|
@@ -14,6 +15,7 @@ import { supabaseSelectTypedColumnsRule } from "./supabase-select-typed-columns.
|
|
|
14
15
|
/** Single plugin object to avoid ESLint "Cannot redefine plugin" errors. */
|
|
15
16
|
export const localPlugin = {
|
|
16
17
|
rules: {
|
|
18
|
+
"entry-single-service-call": entrySingleServiceCallRule,
|
|
17
19
|
"entry-template": entryTemplateRule,
|
|
18
20
|
"feature-name": featureNameRule,
|
|
19
21
|
"form-state-naming": formStateNamingRule,
|
package/src/common/naming.mjs
CHANGED
|
@@ -305,6 +305,15 @@ export function createNamingConfigs(featureRoot, prefixLibMapping) {
|
|
|
305
305
|
},
|
|
306
306
|
});
|
|
307
307
|
|
|
308
|
+
configs.push({
|
|
309
|
+
name: "naming/entry-single-service-call",
|
|
310
|
+
files: featuresGlob(featureRoot, "**/entries/*.ts"),
|
|
311
|
+
plugins: { local: localPlugin },
|
|
312
|
+
rules: {
|
|
313
|
+
"local/entry-single-service-call": "error",
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
|
|
308
317
|
configs.push(
|
|
309
318
|
{
|
|
310
319
|
name: "naming/entries-shared",
|
package/src/next/tailwindcss.mjs
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { dirname, join, sep } from "node:path";
|
|
3
|
+
|
|
1
4
|
import betterTailwindcss from "eslint-plugin-better-tailwindcss";
|
|
2
5
|
|
|
6
|
+
// `eslint-plugin-better-tailwindcss` resolves a relative `entryPoint` against
|
|
7
|
+
// the linter's `cwd`, which under LSP servers (vscode-eslint, Zed) is often
|
|
8
|
+
// the edited file's directory rather than the consumer's project root and
|
|
9
|
+
// breaks resolution. Mirror the `findProjectRoot` pattern from
|
|
10
|
+
// `common/rules.mjs` and `common/constants.mjs`: walk up from this module
|
|
11
|
+
// outside of `node_modules` and locate `src/app/globals.css`, then pass the
|
|
12
|
+
// absolute path so resolution is cwd-independent. Falls back to the relative
|
|
13
|
+
// path when not found, preserving the previous CLI-only behavior.
|
|
14
|
+
const findEntryPoint = (start) => {
|
|
15
|
+
let dir = start;
|
|
16
|
+
while (dir !== dirname(dir)) {
|
|
17
|
+
if (!dir.split(sep).includes("node_modules")) {
|
|
18
|
+
const candidate = join(dir, "src/app/globals.css");
|
|
19
|
+
if (existsSync(candidate)) {
|
|
20
|
+
return candidate;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
dir = dirname(dir);
|
|
24
|
+
}
|
|
25
|
+
return "src/app/globals.css";
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const entryPoint = findEntryPoint(import.meta.dirname);
|
|
29
|
+
|
|
3
30
|
/**
|
|
4
31
|
* Tailwind CSS v4 lint rules:
|
|
5
32
|
*
|
|
@@ -9,9 +36,9 @@ import betterTailwindcss from "eslint-plugin-better-tailwindcss";
|
|
|
9
36
|
* (`-space-x-2`) remain allowed for intentional overlap
|
|
10
37
|
* - class order, deprecated classes, conflicts, duplicates, and whitespace
|
|
11
38
|
* are enforced via `eslint-plugin-better-tailwindcss`
|
|
12
|
-
* - `entryPoint`
|
|
13
|
-
*
|
|
14
|
-
* the
|
|
39
|
+
* - `entryPoint` is auto-resolved to the consuming project's
|
|
40
|
+
* `src/app/globals.css` via walk-up from this module. Override in the
|
|
41
|
+
* project's eslint.config.mjs if the file lives elsewhere.
|
|
15
42
|
*/
|
|
16
43
|
export const tailwindcssConfigs = [
|
|
17
44
|
{
|
|
@@ -20,7 +47,7 @@ export const tailwindcssConfigs = [
|
|
|
20
47
|
plugins: { "better-tailwindcss": betterTailwindcss },
|
|
21
48
|
settings: {
|
|
22
49
|
"better-tailwindcss": {
|
|
23
|
-
entryPoint
|
|
50
|
+
entryPoint,
|
|
24
51
|
},
|
|
25
52
|
},
|
|
26
53
|
rules: {
|