@yasainet/eslint 0.0.78 → 0.0.80
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 +1 -1
- package/src/cli/test-audit.mjs +76 -19
- package/src/common/_internal/constants.mjs +1 -1
- package/src/common/cross-cutting/jsdoc.mjs +8 -1
- package/src/common/cross-cutting/tests.mjs +39 -0
- package/src/common/index.mjs +3 -0
- package/src/common/local-plugins/feature-name.mjs +2 -2
package/package.json
CHANGED
package/src/cli/test-audit.mjs
CHANGED
|
@@ -2,14 +2,18 @@
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
|
-
// pure layer (schemas / utils) の unit test presence
|
|
5
|
+
// pure layer (schemas / utils) の unit test presence と、entries を持つ feature の
|
|
6
|
+
// e2e presence を機械チェックする:
|
|
6
7
|
//
|
|
7
|
-
// - 各 source に 兄弟 *.test.ts が存在するか、`@unit-exempt:` marker を持つことを要求
|
|
8
|
-
//
|
|
8
|
+
// - unit: 各 source に 兄弟 *.test.ts が存在するか、`@unit-exempt:` marker を持つことを要求
|
|
9
|
+
// - schemas は定義上 pure。utils は impure 混在のため marker で opt-out できる
|
|
10
|
+
// - e2e: entries/ を持つ feature ごとに tests/e2e/<feature>/*.spec.ts の存在を要求
|
|
11
|
+
// - 配線 (entries) は unit でなく e2e で疎通を担保する方針の存在チェック
|
|
9
12
|
// - ESLint の per-file モデルと噛み合わない「存在強制」を全ツリー一括監査で担う
|
|
10
13
|
|
|
11
14
|
const REQUIRE_DIRS = new Set(["schemas", "utils"]);
|
|
12
15
|
const EXEMPT_RE = /@unit-exempt:/;
|
|
16
|
+
const E2E_ROOT = "tests/e2e";
|
|
13
17
|
|
|
14
18
|
function getFlag(argv, name) {
|
|
15
19
|
const i = argv.indexOf(name);
|
|
@@ -48,49 +52,102 @@ function isSatisfied(file) {
|
|
|
48
52
|
return EXEMPT_RE.test(fs.readFileSync(file, "utf8"));
|
|
49
53
|
}
|
|
50
54
|
|
|
55
|
+
/** pure layer (schemas / utils) の unit test 不在を列挙する */
|
|
56
|
+
function auditUnit(projectRoot, featuresDir) {
|
|
57
|
+
const violations = [];
|
|
58
|
+
for (const file of walk(featuresDir)) {
|
|
59
|
+
if (isTarget(file) && !isSatisfied(file)) {
|
|
60
|
+
violations.push(path.relative(projectRoot, file));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return violations.sort();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** entries/ を持つ feature ごとに対応する e2e spec の不在を列挙する */
|
|
67
|
+
function auditE2e(projectRoot, featureRoot, featuresDir) {
|
|
68
|
+
if (!fs.existsSync(featuresDir)) {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
const violations = [];
|
|
72
|
+
for (const entry of fs.readdirSync(featuresDir, { withFileTypes: true })) {
|
|
73
|
+
if (!entry.isDirectory()) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const feature = entry.name;
|
|
77
|
+
const hasEntries = fs.existsSync(path.join(featuresDir, feature, "entries"));
|
|
78
|
+
if (!hasEntries) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const e2eDir = path.join(projectRoot, E2E_ROOT, feature);
|
|
82
|
+
const hasSpec = [...walk(e2eDir)].some((f) => f.endsWith(".spec.ts"));
|
|
83
|
+
if (!hasSpec) {
|
|
84
|
+
violations.push(
|
|
85
|
+
`${featureRoot}/${feature}/entries → ${E2E_ROOT}/${feature}/*.spec.ts`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return violations.sort();
|
|
90
|
+
}
|
|
91
|
+
|
|
51
92
|
function main() {
|
|
52
93
|
const argv = process.argv.slice(2);
|
|
53
94
|
|
|
54
95
|
if (argv.includes("--help") || argv.includes("-h")) {
|
|
55
96
|
console.log(
|
|
56
|
-
"test-audit — pure layer
|
|
97
|
+
"test-audit — pure layer の unit test と feature の e2e presence を検査\n\n" +
|
|
57
98
|
"Usage: test-audit [--feature-root <path>]\n\n" +
|
|
58
99
|
" --feature-root <path> feature root (default: src/features)\n\n" +
|
|
59
|
-
"各 schemas/*.ts と utils/*.ts に 兄弟 *.test.ts または\n" +
|
|
60
|
-
"`// @unit-exempt: <理由>` marker
|
|
100
|
+
" unit: 各 schemas/*.ts と utils/*.ts に 兄弟 *.test.ts または\n" +
|
|
101
|
+
" `// @unit-exempt: <理由>` marker を要求する。\n" +
|
|
102
|
+
" e2e: entries/ を持つ feature ごとに\n" +
|
|
103
|
+
" tests/e2e/<feature>/*.spec.ts を要求する。",
|
|
61
104
|
);
|
|
62
105
|
process.exit(0);
|
|
63
106
|
}
|
|
64
107
|
|
|
65
108
|
const projectRoot = process.cwd();
|
|
66
109
|
const featureRoot = getFlag(argv, "--feature-root") ?? "src/features";
|
|
67
|
-
const
|
|
110
|
+
const featuresDir = path.join(projectRoot, featureRoot);
|
|
68
111
|
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
112
|
+
const unitViolations = auditUnit(projectRoot, featuresDir);
|
|
113
|
+
const e2eViolations = auditE2e(projectRoot, featureRoot, featuresDir);
|
|
114
|
+
|
|
115
|
+
let failed = false;
|
|
116
|
+
|
|
117
|
+
if (unitViolations.length > 0) {
|
|
118
|
+
failed = true;
|
|
119
|
+
console.error(
|
|
120
|
+
`✗ test-audit(unit): ${unitViolations.length} 件の pure layer に unit test も @unit-exempt marker もありません:\n`,
|
|
121
|
+
);
|
|
122
|
+
for (const v of unitViolations) {
|
|
123
|
+
console.error(` ${v}`);
|
|
75
124
|
}
|
|
125
|
+
console.error(
|
|
126
|
+
"\n対応: 兄弟 *.test.ts を追加するか、impure な場合は\n" +
|
|
127
|
+
"`// @unit-exempt: <理由>` を記載する。\n",
|
|
128
|
+
);
|
|
76
129
|
}
|
|
77
130
|
|
|
78
|
-
if (
|
|
131
|
+
if (e2eViolations.length > 0) {
|
|
132
|
+
failed = true;
|
|
79
133
|
console.error(
|
|
80
|
-
`✗ test-audit: ${
|
|
134
|
+
`✗ test-audit(e2e): ${e2eViolations.length} 件の feature に対応する e2e spec がありません:\n`,
|
|
81
135
|
);
|
|
82
|
-
for (const v of
|
|
136
|
+
for (const v of e2eViolations) {
|
|
83
137
|
console.error(` ${v}`);
|
|
84
138
|
}
|
|
85
139
|
console.error(
|
|
86
|
-
"\n対応:
|
|
87
|
-
"
|
|
140
|
+
"\n対応: tests/e2e/<feature>/*.spec.ts を追加する\n" +
|
|
141
|
+
"(配線の疎通は e2e で担保する)。\n",
|
|
88
142
|
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (failed) {
|
|
89
146
|
process.exit(1);
|
|
90
147
|
}
|
|
91
148
|
|
|
92
149
|
console.log(
|
|
93
|
-
"✓ test-audit: schemas / utils
|
|
150
|
+
"✓ test-audit: schemas / utils の unit と entries feature の e2e はすべて揃っています。",
|
|
94
151
|
);
|
|
95
152
|
}
|
|
96
153
|
|
|
@@ -20,7 +20,7 @@ function findProjectRoot() {
|
|
|
20
20
|
|
|
21
21
|
const PROJECT_ROOT = findProjectRoot();
|
|
22
22
|
|
|
23
|
-
const EXCLUDE_LIST = ["
|
|
23
|
+
const EXCLUDE_LIST = ["types.ts", "proxy.ts", "utils.ts"];
|
|
24
24
|
|
|
25
25
|
export function generatePrefixLibMapping(featureRoot) {
|
|
26
26
|
const libRoot = featureRoot.replace(/features$/, "lib");
|
|
@@ -2,6 +2,14 @@ import jsdocPlugin from "eslint-plugin-jsdoc";
|
|
|
2
2
|
|
|
3
3
|
import { featuresGlob } from "../_internal/constants.mjs";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* queries / services の public 関数に JSDoc を必須化する:
|
|
7
|
+
*
|
|
8
|
+
* - 「非自明か」の免除判断を作らせないため require を決定論的に強制する
|
|
9
|
+
* - 冗長でも全関数で強制し例外ゼロ=判断ゼロを優先する
|
|
10
|
+
* - description が Why-not か What かは機械判断できず Claude に委ねる
|
|
11
|
+
* - TS が型の真実源なので tags は強制しない(@param 等は二重写し)
|
|
12
|
+
*/
|
|
5
13
|
export function createJsdocConfigs({ featureRoot }) {
|
|
6
14
|
return [
|
|
7
15
|
{
|
|
@@ -9,7 +17,6 @@ export function createJsdocConfigs({ featureRoot }) {
|
|
|
9
17
|
files: [
|
|
10
18
|
...featuresGlob(featureRoot, "**/queries/*.ts"),
|
|
11
19
|
...featuresGlob(featureRoot, "**/services*/*.ts"),
|
|
12
|
-
...featuresGlob(featureRoot, "**/utils*/*.ts"),
|
|
13
20
|
],
|
|
14
21
|
plugins: {
|
|
15
22
|
jsdoc: jsdocPlugin,
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* unit test (utils / schemas の兄弟 *.test.ts) から配線層への import を禁止する:
|
|
3
|
+
*
|
|
4
|
+
* - services / queries / entries を import すると mock の echo(写経)になる
|
|
5
|
+
* - pure (utils / schemas) のみを対象にし、配線の検証は e2e に委ねる
|
|
6
|
+
* - createUtilsConfigs より後に spread し、test.ts では本 rule を後勝ちにする
|
|
7
|
+
*/
|
|
8
|
+
export function createTestsConfigs({ featureRoot }) {
|
|
9
|
+
return [
|
|
10
|
+
{
|
|
11
|
+
name: "test/unit-no-wiring-import",
|
|
12
|
+
files: [`${featureRoot}/**/*.test.{ts,tsx}`],
|
|
13
|
+
rules: {
|
|
14
|
+
"no-restricted-imports": [
|
|
15
|
+
"error",
|
|
16
|
+
{
|
|
17
|
+
patterns: [
|
|
18
|
+
{
|
|
19
|
+
group: [
|
|
20
|
+
"**/services/*",
|
|
21
|
+
"**/services",
|
|
22
|
+
"**/queries/*",
|
|
23
|
+
"**/queries",
|
|
24
|
+
"**/entries/*",
|
|
25
|
+
"**/entries",
|
|
26
|
+
],
|
|
27
|
+
message:
|
|
28
|
+
"unit test は配線層 (services / queries / entries) を import 不可。" +
|
|
29
|
+
"mock の echo になる:\n" +
|
|
30
|
+
"- pure (utils / schemas) のみ unit する\n" +
|
|
31
|
+
"- 配線の検証は e2e に委ねる",
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
}
|
package/src/common/index.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { createNamespaceImportConfigs } from "./cross-cutting/namespace-import.m
|
|
|
11
11
|
import { createNoAnyReturnConfigs } from "./cross-cutting/no-any-return.mjs";
|
|
12
12
|
import { createNoColocatedTestConfigs } from "./cross-cutting/no-colocated-test.mjs";
|
|
13
13
|
import { createSupabaseColumnsSatisfiesConfigs } from "./cross-cutting/supabase-columns-satisfies.mjs";
|
|
14
|
+
import { createTestsConfigs } from "./cross-cutting/tests.mjs";
|
|
14
15
|
import { createConstantsConfigs } from "./layers/constants.mjs";
|
|
15
16
|
import { createEntriesConfigs } from "./layers/entries.mjs";
|
|
16
17
|
import { createTopLevelLibConfigs } from "./layers/top-level/lib.mjs";
|
|
@@ -47,6 +48,8 @@ export function createCommonConfigs(
|
|
|
47
48
|
...createEntriesConfigs(ctx),
|
|
48
49
|
...createFeaturesTsOnlyConfigs(ctx),
|
|
49
50
|
...createNoColocatedTestConfigs(ctx),
|
|
51
|
+
// createUtilsConfigs より後に置き、test.ts への配線 import 禁止を後勝ちにする。
|
|
52
|
+
...createTestsConfigs(ctx),
|
|
50
53
|
...createNoAnyReturnConfigs(ctx),
|
|
51
54
|
...createFeatureDefaultImportsConfigs(ctx),
|
|
52
55
|
...createJsdocConfigs(ctx),
|
|
@@ -95,9 +95,9 @@ export const featureNameRule = {
|
|
|
95
95
|
const candidateTypePaths = [
|
|
96
96
|
path.join(
|
|
97
97
|
projectRoot,
|
|
98
|
-
featureRoot.replace(/features$/, "lib/supabase/
|
|
98
|
+
featureRoot.replace(/features$/, "lib/supabase/types.ts"),
|
|
99
99
|
),
|
|
100
|
-
path.join(projectRoot, "src/lib/supabase/
|
|
100
|
+
path.join(projectRoot, "src/lib/supabase/types.ts"),
|
|
101
101
|
];
|
|
102
102
|
const supabaseTypePath =
|
|
103
103
|
candidateTypePaths.find((p) => fs.existsSync(p)) ?? candidateTypePaths[0];
|