eslint-plugin-smarthr 1.3.0 → 1.4.1

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/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ## [1.4.1](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v1.4.0...eslint-plugin-smarthr-v1.4.1) (2025-03-25)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * tailwind-variantsでtv以外のimportを行った際、エラーになる問題を修正する ([#536](https://github.com/kufu/tamatebako/issues/536)) ([83efcf6](https://github.com/kufu/tamatebako/commit/83efcf671938ca471265af9c05bd47fbcce55fb9))
11
+
12
+ ## [1.4.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v1.3.0...eslint-plugin-smarthr-v1.4.0) (2025-03-06)
13
+
14
+
15
+ ### Features
16
+
17
+ * best-practice-for-tailwind-prohibit-root-margin ([#523](https://github.com/kufu/tamatebako/issues/523)) ([b14700a](https://github.com/kufu/tamatebako/commit/b14700a17b1c5496e50c6c920f0281bf3ec1d7da))
18
+
5
19
  ## [1.3.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v1.2.0...eslint-plugin-smarthr-v1.3.0) (2025-02-21)
6
20
 
7
21
 
package/README.md CHANGED
@@ -22,6 +22,7 @@
22
22
  - [best-practice-for-layouts](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-layouts)
23
23
  - [best-practice-for-remote-trigger-dialog](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-remote-trigger-dialog)
24
24
  - [best-practice-for-tailwind-variants](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-tailwind-variants)
25
+ - [best-practice-for-tailwind-prohibit-root-margin](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-tailwind-prohibit-root-margin)
25
26
  - [design-system-guideline-prohibit-double-icons](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/design-system-guideline-prohibit-double-icons)
26
27
  - [format-import-path](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/format-import-path)
27
28
  - [format-translate-component](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/format-translate-component)
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "eslint-plugin-smarthr",
3
- "version": "1.3.0",
3
+ "version": "1.4.1",
4
4
  "author": "SmartHR",
5
5
  "license": "MIT",
6
6
  "description": "A sharable ESLint plugin for SmartHR",
7
7
  "main": "index.js",
8
8
  "engines": {
9
- "node": ">=20.18.2"
9
+ "node": ">=20.18.3"
10
10
  },
11
11
  "scripts": {
12
12
  "test": "jest"
@@ -37,5 +37,5 @@
37
37
  "eslintplugin",
38
38
  "smarthr"
39
39
  ],
40
- "gitHead": "3c7f6f40f11714a9dd6cb88dc243d1b6973c6a93"
40
+ "gitHead": "30434f9943f7dd03272ca3100497c4c3c6bb6fc0"
41
41
  }
@@ -0,0 +1,73 @@
1
+ # best-practice-for-tailwind-prohibit-root-margin
2
+
3
+ tailwindcss を使用したコンポーネントのルート要素に外側の余白(margin)を設定することを禁止します。
4
+
5
+ ## ルールの目的
6
+
7
+ コンポーネントは、それ自体が外側に余白を持つべきではありません。外側の余白の制御は、コンポーネントを使用する側の責任とすべきです。
8
+
9
+ これにより、以下のメリットがあります。
10
+
11
+ - コンポーネントの再利用性が向上します
12
+ - レイアウトの一貫性が保たれます
13
+ - コンポーネント間の余白の調整が容易になります
14
+
15
+ ## 対象となるクラス名
16
+
17
+ 以下のパターンのクラス名が検出対象となります。
18
+
19
+ - `shr-m-`
20
+ - `shr-mt-`
21
+ - `shr-mr-`
22
+ - `shr-mb-`
23
+ - `shr-ml-`
24
+
25
+ クラス名のプレフィックス(`shr-`) の通り、SmartHR UI と合わせて使用されることを前提としています。
26
+
27
+ ## NG例
28
+
29
+ ```jsx
30
+ const Button = () => {
31
+ return <button className="shr-m-4">Click me</button>
32
+ }
33
+
34
+ const Card = () => {
35
+ return (
36
+ <div className="shr-mt-4">
37
+ <p>Content</p>
38
+ </div>
39
+ )
40
+ }
41
+ ```
42
+
43
+ ## OK例
44
+
45
+ ```jsx
46
+ const Button = ({ className }) => {
47
+ return <button className={className}>Click me</button>
48
+ }
49
+
50
+ const Card = () => {
51
+ return (
52
+ <div>
53
+ <p>Content</p>
54
+ </div>
55
+ )
56
+ }
57
+
58
+ // 使用する側で余白を制御
59
+ const Page = () => {
60
+ return (
61
+ <div>
62
+ <Button className="shr-m-4" />
63
+ <div className="shr-mt-4">
64
+ <Card />
65
+ </div>
66
+ </div>
67
+ )
68
+ }
69
+ ```
70
+
71
+ ## オプション
72
+
73
+ このルールにはオプションはありません。
@@ -0,0 +1,103 @@
1
+ const { AST_NODE_TYPES } = require('@typescript-eslint/utils')
2
+
3
+ const SCHEMA = []
4
+ const MARGIN_CLASS_PATTERNS = /shr-m[trbl]?-/ // mt-, mr-, mb-, ml-, m-
5
+
6
+ /**
7
+ * コンポーネントのルート要素を渡し、該当の余白クラスが存在すればそれを、なければNULLを返す
8
+ * @param {import('@typescript-eslint/utils').TSESTree.Node} node
9
+ * @returns {import('@typescript-eslint/utils').TSESTree.Literal | null}
10
+ */
11
+ const findSpacingClassInRootElement = (node) => {
12
+ // JSX でなければ対象外
13
+ if (node.type !== AST_NODE_TYPES.JSXElement) return null
14
+
15
+ // className属性がなければ対象外
16
+ const classNameAttr = node.openingElement.attributes.find(
17
+ (attr) => attr.type === AST_NODE_TYPES.JSXAttribute && attr.name.name === 'className',
18
+ )
19
+ if (!classNameAttr) return null
20
+
21
+ // className属性の値がリテラルでなければ対象外
22
+ if (classNameAttr?.value?.type !== AST_NODE_TYPES.Literal || typeof classNameAttr.value.value !== 'string') return null
23
+
24
+ // className属性の値に余白クラスが含まれていればそれを返す
25
+ return MARGIN_CLASS_PATTERNS.test(classNameAttr.value.value) ? classNameAttr.value : null
26
+ }
27
+
28
+ /**
29
+ * ブロックステートメント内から、JSX要素を返す ReturnStatement を返す
30
+ * @param {import('@typescript-eslint/utils').TSESTree.BlockStatement} block
31
+ * @returns {import('@typescript-eslint/utils').TSESTree.ReturnStatement | null}
32
+ */
33
+ const findJSXReturnStatement = (block) => {
34
+ for (const statement of block.body) {
35
+ if (statement.type === AST_NODE_TYPES.ReturnStatement && statement.argument?.type === AST_NODE_TYPES.JSXElement) {
36
+ return statement
37
+ }
38
+ }
39
+ return null
40
+ }
41
+
42
+ /**
43
+ * @type {import('@typescript-eslint/utils').TSESLint.RuleModule}
44
+ */
45
+ module.exports = {
46
+ meta: {
47
+ type: 'problem',
48
+ schema: SCHEMA,
49
+ messages: {
50
+ noRootSpacing:
51
+ 'コンポーネントのルート要素に外側への余白(margin)を設定しないでください。外側の余白は使用する側で制御するべきです。',
52
+ },
53
+ },
54
+ create(context) {
55
+ /**
56
+ * 関数本体をチェックし、ルート要素で余白クラスが設定されたJSXを返している場合、エラーを報告する
57
+ * @param {import('@typescript-eslint/utils').TSESTree.Node} body
58
+ */
59
+ const checkFunctionBody = (body) => {
60
+ // 関数がブロックを持たずに直接JSXを返すパターン
61
+ if (body.type === AST_NODE_TYPES.JSXElement) {
62
+ const spacingClass = findSpacingClassInRootElement(body)
63
+ if (spacingClass) {
64
+ context.report({
65
+ node: spacingClass,
66
+ messageId: 'noRootSpacing',
67
+ })
68
+ }
69
+ return
70
+ }
71
+
72
+ // 関数がブロック内で JSX を return するパターン
73
+ if (body.type === AST_NODE_TYPES.BlockStatement) {
74
+ const returnStatement = findJSXReturnStatement(body)
75
+ if (returnStatement?.argument) {
76
+ const spacingClass = findSpacingClassInRootElement(returnStatement.argument)
77
+ if (spacingClass) {
78
+ context.report({
79
+ node: spacingClass,
80
+ messageId: 'noRootSpacing',
81
+ })
82
+ }
83
+ }
84
+ }
85
+ }
86
+
87
+ return {
88
+ // アロー関数式のコンポーネントをチェック
89
+ ArrowFunctionExpression: (node) => {
90
+ if (node.parent?.type === AST_NODE_TYPES.VariableDeclarator) {
91
+ checkFunctionBody(node.body)
92
+ }
93
+ },
94
+
95
+ // function宣言のコンポーネントをチェック
96
+ FunctionDeclaration: (node) => {
97
+ checkFunctionBody(node.body)
98
+ },
99
+ }
100
+ },
101
+ }
102
+
103
+ module.exports.schema = SCHEMA
@@ -4,7 +4,7 @@ const TV_COMPONENTS_METHOD = 'tv'
4
4
  const TV_COMPONENTS = 'tailwind-variants'
5
5
  const TV_RESULT_CONST_NAME_REGEX = /(C|c)lassNameGenerator$/
6
6
 
7
- const findValidImportNameNode = (s) => s.type === 'ImportSpecifier' && s.local.name === TV_COMPONENTS_METHOD
7
+ const findValidImportNameNode = (s) => s.type === 'ImportSpecifier' && s.imported.name === TV_COMPONENTS_METHOD && s.local.name !== TV_COMPONENTS_METHOD
8
8
 
9
9
  const checkImportTailwindVariants = (node, context) => {
10
10
  }
@@ -43,7 +43,7 @@ module.exports = {
43
43
  return {
44
44
  ImportDeclaration: (node) => {
45
45
  if (node.source.value === TV_COMPONENTS) {
46
- if (!node.specifiers.some(findValidImportNameNode)) {
46
+ if (node.specifiers.some(findValidImportNameNode)) {
47
47
  context.report({
48
48
  node,
49
49
  message: `${TV_COMPONENTS} をimportする際は、名称が"${TV_COMPONENTS_METHOD}" となるようにしてください。例: "import { ${TV_COMPONENTS_METHOD} } from '${TV_COMPONENTS}'"`,
@@ -0,0 +1,111 @@
1
+ const rule = require('../rules/best-practice-for-tailwind-prohibit-root-margin')
2
+ const RuleTester = require('eslint').RuleTester
3
+
4
+ const ruleTester = new RuleTester({
5
+ languageOptions: {
6
+ parserOptions: {
7
+ ecmaFeatures: {
8
+ jsx: true,
9
+ },
10
+ },
11
+ },
12
+ })
13
+
14
+ const errorMessage =
15
+ 'コンポーネントのルート要素に外側への余白(margin)を設定しないでください。外側の余白は使用する側で制御するべきです。'
16
+
17
+ ruleTester.run('best-practice-for-tailwind-prohibit-root-margin', rule, {
18
+ valid: [
19
+ // 余白のないコンポーネント
20
+ {
21
+ code: `
22
+ const Button = () => {
23
+ return <button className="shr-bg-blue-500">Click me</button>
24
+ }
25
+ `,
26
+ },
27
+ // コンポーネントのルート以外の要素での余白使用
28
+ {
29
+ code: `
30
+ const Card = () => {
31
+ return (
32
+ <div className="shr-bg-white">
33
+ <h2 className="shr-mt-4">Title</h2>
34
+ <p className="shr-mb-2 shr-pt-2">Content</p>
35
+ </div>
36
+ )
37
+ }
38
+ `,
39
+ },
40
+ // shr-min などの、shr-m から始まるほかのクラス
41
+ { code: `const Button = () => <button className="shr-min-w-100">Click me</button>` },
42
+ { code: `const Button = () => <button className="shr-min-h-100">Click me</button>` },
43
+ { code: `const Button = () => <button className="shr-max-w-100">Click me</button>` },
44
+ { code: `const Button = () => <button className="shr-max-h-100">Click me</button>` },
45
+ // padding は許可
46
+ { code: `const Button = () => <button className="shr-p-4">Click me</button>` },
47
+ { code: `const Button = () => <button className="shr-pt-4">Click me</button>` },
48
+ { code: `const Button = () => <button className="shr-pb-4">Click me</button>` },
49
+ { code: `const Button = () => <button className="shr-pl-4">Click me</button>` },
50
+ { code: `const Button = () => <button className="shr-pr-4">Click me</button>` },
51
+ // リテラルでないクラス名
52
+ { code: `const Button = () => <button className={shr-mt-2}>Click me</button>` },
53
+ { code: `const Button = () => <button className={shr-pb-4}>Click me</button>` },
54
+ ],
55
+ invalid: [
56
+ // マージンクラスを持つコンポーネント
57
+ {
58
+ code: `const Button = () => { return <button className="shr-m-4">Click me</button> }`,
59
+ errors: [{ message: errorMessage }],
60
+ },
61
+ {
62
+ code: `const Button = () => <button className="shr-mt-4">Click me</button>`,
63
+ errors: [{ message: errorMessage }],
64
+ },
65
+ {
66
+ code: `const Button = () => { return <button className="shr-mb-4">Click me</button> }`,
67
+ errors: [{ message: errorMessage }],
68
+ },
69
+ {
70
+ code: `const Button = () => <button className="shr-ml-4">Click me</button>`,
71
+ errors: [{ message: errorMessage }],
72
+ },
73
+ {
74
+ code: `const Button = () => { return <button className="shr-mr-4">Click me</button> }`,
75
+ errors: [{ message: errorMessage }],
76
+ },
77
+ // 複数の余白クラスを持つコンポーネント
78
+ {
79
+ code: `
80
+ const Card = () => {
81
+ return (
82
+ <div className="shr-mt-4 shr-mb-2 shr-ml-2 shr-mr-2">
83
+ <p>Content</p>
84
+ </div>
85
+ )
86
+ }
87
+ `,
88
+ errors: [{ message: errorMessage }],
89
+ },
90
+ // 他のクラスと組み合わせた余白
91
+ {
92
+ code: `
93
+ const Box = () => (
94
+ <div className="shr-bg-gray-100 shr-ml-2">
95
+ <p>Content</p>
96
+ </div>
97
+ )
98
+ `,
99
+ errors: [{ message: errorMessage }],
100
+ },
101
+ // function 宣言によるコンポーネント
102
+ {
103
+ code: `
104
+ function Button() {
105
+ return <button className="shr-m-4">Click me</button>
106
+ }
107
+ `,
108
+ errors: [{ message: errorMessage }],
109
+ },
110
+ ],
111
+ })
@@ -14,6 +14,7 @@ const ruleTester = new RuleTester({
14
14
  ruleTester.run('best-practice-for-button-element', rule, {
15
15
  valid: [
16
16
  { code: `import { tv } from 'tailwind-variants'` },
17
+ { code: `import { defaultConfig } from 'tailwind-variants'` },
17
18
  { code: `const classNameGenerator = tv()` },
18
19
  { code: `const xxxClassNameGenerator = tv()` },
19
20
  { code: `const hoge = useMemo(() => classNameGenerator(), [])` },