@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yasainet/eslint",
3
- "version": "0.0.78",
3
+ "version": "0.0.80",
4
4
  "description": "ESLint",
5
5
  "type": "module",
6
6
  "exports": {
@@ -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
- // - schemas は定義上 pure。utils は impure 混在のため marker で opt-out できる
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 (schemas/utils) の unit test presence を検査\n\n" +
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 roots = [path.join(projectRoot, featureRoot)];
110
+ const featuresDir = path.join(projectRoot, featureRoot);
68
111
 
69
- const violations = [];
70
- for (const root of roots) {
71
- for (const file of walk(root)) {
72
- if (isTarget(file) && !isSatisfied(file)) {
73
- violations.push(path.relative(projectRoot, file));
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 (violations.length > 0) {
131
+ if (e2eViolations.length > 0) {
132
+ failed = true;
79
133
  console.error(
80
- `✗ test-audit: ${violations.length} 件の pure layer unit test も @unit-exempt marker もありません:\n`,
134
+ `✗ test-audit(e2e): ${e2eViolations.length} 件の feature に対応する e2e spec がありません:\n`,
81
135
  );
82
- for (const v of violations.sort()) {
136
+ for (const v of e2eViolations) {
83
137
  console.error(` ${v}`);
84
138
  }
85
139
  console.error(
86
- "\n対応: 兄弟 *.test.ts を追加するか、impure な場合は\n" +
87
- "`// @unit-exempt: <理由>` を記載する。",
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 はすべて unit test または @unit-exempt が揃っています。",
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 = ["type.ts", "proxy.ts", "utils.ts"];
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
+ }
@@ -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/type.ts"),
98
+ featureRoot.replace(/features$/, "lib/supabase/types.ts"),
99
99
  ),
100
- path.join(projectRoot, "src/lib/supabase/type.ts"),
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];