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 +14 -0
- package/README.md +1 -0
- package/package.json +3 -3
- package/rules/best-practice-for-tailwind-prohibit-root-margin/README.md +73 -0
- package/rules/best-practice-for-tailwind-prohibit-root-margin/index.js +103 -0
- package/rules/best-practice-for-tailwind-variants/index.js +2 -2
- package/test/best-practice-for-tailwind-prohibit-root-margin.js +111 -0
- package/test/best-practice-for-tailwind-variants.js +1 -0
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
|
+
"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.
|
|
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": "
|
|
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.
|
|
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 (
|
|
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(), [])` },
|