@yasainet/eslint 0.0.59 → 0.0.60

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.59",
3
+ "version": "0.0.60",
4
4
  "description": "ESLint",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,7 @@
1
1
  import { featureNameRule } from "./feature-name.mjs";
2
2
  import { formStateNamingRule } from "./form-state-naming.mjs";
3
3
  import { importPathStyleRule } from "./import-path-style.mjs";
4
+ import { layoutMainStructuralOnlyRule } from "./layout-main-structural-only.mjs";
4
5
  import { namespaceImportNameRule } from "./namespace-import-name.mjs";
5
6
  import { noAnyReturnRule } from "./no-any-return.mjs";
6
7
  import { queriesExportRule } from "./queries-export.mjs";
@@ -15,6 +16,7 @@ export const localPlugin = {
15
16
  "feature-name": featureNameRule,
16
17
  "form-state-naming": formStateNamingRule,
17
18
  "import-path-style": importPathStyleRule,
19
+ "layout-main-structural-only": layoutMainStructuralOnlyRule,
18
20
  "namespace-import-name": namespaceImportNameRule,
19
21
  "no-any-return": noAnyReturnRule,
20
22
  "queries-export": queriesExportRule,
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Forbid decoration utility classes on the `<main>` element.
3
+ *
4
+ * Design philosophy: `app/**\/layout.tsx` の `<main>` は Header / Footer の
5
+ * 縦積みを受け持つ構造スロット。装飾(余白・間隔)は page.tsx 側の Container 等が
6
+ * 持つべき。main に padding / margin / gap を直接当てると、page 全体に暗黙の
7
+ * オフセットが生まれ、page ごとの調整余地が失われる。
8
+ *
9
+ * Disallowed Tailwind token prefixes (case-sensitive):
10
+ *
11
+ * - `p`, `m`, `py`, `px`, `pt`, `pb`, `pl`, `pr`, `my`, `mx`, `mt`, `mb`, `ml`, `mr` — padding / margin
12
+ * - `space-x`, `space-y` — sibling spacing
13
+ * - `gap` — flex / grid gap
14
+ *
15
+ * Allowed: structural utilities (`flex`, `flex-1`, `flex-col`, `min-h-*`,
16
+ * `block`, `relative`, etc.)
17
+ *
18
+ * className value forms supported: string Literal, TemplateLiteral,
19
+ * `cn(...)` / `clsx(...)` style CallExpression args (string args only).
20
+ */
21
+
22
+ const DISALLOWED_TOKEN = /^(?:[pm][xytrbl]?-|space-[xy]-|gap-)/;
23
+
24
+ function collectStringLiterals(node, out) {
25
+ if (!node) return;
26
+ switch (node.type) {
27
+ case "Literal":
28
+ if (typeof node.value === "string") out.push(node.value);
29
+ return;
30
+ case "TemplateLiteral":
31
+ for (const q of node.quasis) {
32
+ if (typeof q.value.cooked === "string") out.push(q.value.cooked);
33
+ }
34
+ for (const expr of node.expressions) collectStringLiterals(expr, out);
35
+ return;
36
+ case "CallExpression":
37
+ for (const arg of node.arguments) collectStringLiterals(arg, out);
38
+ return;
39
+ case "ConditionalExpression":
40
+ collectStringLiterals(node.consequent, out);
41
+ collectStringLiterals(node.alternate, out);
42
+ return;
43
+ case "LogicalExpression":
44
+ case "BinaryExpression":
45
+ collectStringLiterals(node.left, out);
46
+ collectStringLiterals(node.right, out);
47
+ return;
48
+ case "ArrayExpression":
49
+ for (const el of node.elements) collectStringLiterals(el, out);
50
+ return;
51
+ }
52
+ }
53
+
54
+ function findInvalidTokens(strings) {
55
+ const invalid = [];
56
+ for (const s of strings) {
57
+ for (const token of s.split(/\s+/)) {
58
+ if (token && DISALLOWED_TOKEN.test(token)) {
59
+ invalid.push(token);
60
+ }
61
+ }
62
+ }
63
+ return invalid;
64
+ }
65
+
66
+ export const layoutMainStructuralOnlyRule = {
67
+ meta: {
68
+ type: "problem",
69
+ messages: {
70
+ invalidToken:
71
+ "<main> in layout.tsx must be structural-only. Move spacing/decoration ({{ tokens }}) to page.tsx (e.g. <Container className=\"py-8\">).",
72
+ },
73
+ schema: [],
74
+ },
75
+ create(context) {
76
+ return {
77
+ JSXOpeningElement(node) {
78
+ if (node.name?.type !== "JSXIdentifier") return;
79
+ if (node.name.name !== "main") return;
80
+
81
+ for (const attr of node.attributes) {
82
+ if (attr.type !== "JSXAttribute") continue;
83
+ if (attr.name?.name !== "className") continue;
84
+
85
+ const strings = [];
86
+ if (attr.value?.type === "Literal") {
87
+ collectStringLiterals(attr.value, strings);
88
+ } else if (attr.value?.type === "JSXExpressionContainer") {
89
+ collectStringLiterals(attr.value.expression, strings);
90
+ }
91
+
92
+ const invalid = findInvalidTokens(strings);
93
+ if (invalid.length > 0) {
94
+ context.report({
95
+ node: attr,
96
+ messageId: "invalidToken",
97
+ data: { tokens: invalid.join(", ") },
98
+ });
99
+ }
100
+ }
101
+ },
102
+ };
103
+ },
104
+ };
@@ -10,6 +10,7 @@ import {
10
10
 
11
11
  import { directivesConfigs } from "./directives.mjs";
12
12
  import { importPathStyleConfigs } from "./imports.mjs";
13
+ import { layoutsConfigs } from "./layouts.mjs";
13
14
  import { namingConfigs } from "./naming.mjs";
14
15
 
15
16
  const nextEntryPointConfigs = createEntryPointConfigs(
@@ -33,5 +34,6 @@ export const eslintConfig = [
33
34
  ...namingConfigs,
34
35
  ...directivesConfigs,
35
36
  ...importPathStyleConfigs,
37
+ ...layoutsConfigs,
36
38
  ...nextEntryPointConfigs,
37
39
  ];
@@ -0,0 +1,18 @@
1
+ import { localPlugin } from "../common/local-plugins/index.mjs";
2
+
3
+ /**
4
+ * Enforce design rules on Next.js layout files.
5
+ *
6
+ * - `<main>` is a structural slot, not a styling surface. Spacing and
7
+ * decoration belong in page.tsx (e.g. `<Container className="py-8">`).
8
+ */
9
+ export const layoutsConfigs = [
10
+ {
11
+ name: "layouts/main-structural-only",
12
+ files: ["src/app/**/layout.tsx"],
13
+ plugins: { local: localPlugin },
14
+ rules: {
15
+ "local/layout-main-structural-only": "error",
16
+ },
17
+ },
18
+ ];