eslint-plugin-smarthr 0.1.2 → 0.2.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/.github/CODEOWNERS +3 -0
- package/CHANGELOG.md +27 -0
- package/README.md +15 -489
- package/github/CODEOWNERS +3 -0
- package/index.js +3 -6
- package/libs/format_styled_components.js +57 -0
- package/package.json +1 -1
- package/rules/a11y-clickable-element-has-text/README.md +61 -0
- package/rules/a11y-clickable-element-has-text/index.js +71 -0
- package/rules/a11y-image-has-alt-attribute/README.md +55 -0
- package/rules/a11y-image-has-alt-attribute/index.js +48 -0
- package/rules/a11y-trigger-has-button/README.md +57 -0
- package/rules/a11y-trigger-has-button/index.js +74 -0
- package/rules/best-practice-for-date/README.md +40 -0
- package/rules/best-practice-for-date/index.js +42 -0
- package/rules/format-import-path/README.md +99 -0
- package/rules/{format-import-path.js → format-import-path/index.js} +2 -2
- package/rules/format-translate-component/README.md +58 -0
- package/rules/format-translate-component/index.js +97 -0
- package/rules/jsx-start-with-spread-attributes/README.md +31 -0
- package/rules/{jsx-start-with-spread-attributes.js → jsx-start-with-spread-attributes/index.js} +0 -0
- package/rules/no-import-other-domain/README.md +85 -0
- package/rules/{no-import-other-domain.js → no-import-other-domain/index.js} +2 -2
- package/rules/prohibit-export-array-type/README.md +28 -0
- package/rules/prohibit-export-array-type/index.js +28 -0
- package/rules/prohibit-file-name/README.md +35 -0
- package/rules/prohibit-file-name/index.js +61 -0
- package/rules/prohibit-import/README.md +44 -0
- package/rules/{prohibit-import.js → prohibit-import/index.js} +0 -0
- package/rules/redundant-name/README.md +94 -0
- package/rules/{redundant-name.js → redundant-name/index.js} +15 -5
- package/rules/require-barrel-import/README.md +39 -0
- package/rules/{require-barrel-import.js → require-barrel-import/index.js} +1 -1
- package/rules/require-export/README.md +43 -0
- package/rules/require-export/index.js +90 -0
- package/rules/require-import/README.md +51 -0
- package/rules/{require-import.js → require-import/index.js} +0 -0
- package/test/a11y-clickable-element-has-text.js +142 -0
- package/test/a11y-image-has-alt-attribute.js +44 -0
- package/test/a11y-trigger-has-button.js +50 -0
- package/test/best-practice-for-date.js +31 -0
- package/test/format-translate-component.js +37 -0
- package/test/prohibit-file-name.js +45 -0
- package/test/require-export.js +83 -0
- package/rules/a11y-icon-button-has-name.js +0 -56
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const { generateTagFormatter } = require('../../libs/format_styled_components')
|
|
2
|
+
|
|
3
|
+
const EXPECTED_NAMES = {
|
|
4
|
+
'(b|B)utton$': 'Button$',
|
|
5
|
+
'Anchor$': 'Anchor$',
|
|
6
|
+
'Link$': 'Link$',
|
|
7
|
+
'^a$': '(Anchor|Link)$',
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const filterFalsyJSXText = (cs) => cs.filter((c) => (
|
|
11
|
+
!(c.type === 'JSXText' && c.value.match(/^\s*\n+\s*$/))
|
|
12
|
+
))
|
|
13
|
+
|
|
14
|
+
module.exports = {
|
|
15
|
+
meta: {
|
|
16
|
+
type: 'suggestion',
|
|
17
|
+
messages: {
|
|
18
|
+
'format-styled-components': '{{ message }}',
|
|
19
|
+
'a11y-clickable-element-has-text': '{{ message }}',
|
|
20
|
+
},
|
|
21
|
+
schema: [],
|
|
22
|
+
},
|
|
23
|
+
create(context) {
|
|
24
|
+
return {
|
|
25
|
+
...generateTagFormatter({ context, EXPECTED_NAMES }),
|
|
26
|
+
JSXElement: (parentNode) => {
|
|
27
|
+
// HINT: 閉じタグが存在しない === テキストノードが存在しない
|
|
28
|
+
if (!parentNode.closingElement) {
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const node = parentNode.openingElement
|
|
33
|
+
|
|
34
|
+
if (!node.name.name || !node.name.name.match(/^(a|(.*?)Anchor(Button)?|(.*?)Link|(b|B)utton)$/)) {
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const recursiveSearch = (c) => {
|
|
39
|
+
if (['JSXText', 'JSXExpressionContainer'].includes(c.type)) {
|
|
40
|
+
return true
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (c.type === 'JSXElement') {
|
|
44
|
+
if (c.openingElement.attributes.some((a) => (['visuallyHiddenText', 'alt'].includes(a.name.name) && !!a.value.value))) {
|
|
45
|
+
return true
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (c.children && filterFalsyJSXText(c.children).some(recursiveSearch)) {
|
|
49
|
+
return true
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return false
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const child = filterFalsyJSXText(parentNode.children).find(recursiveSearch)
|
|
57
|
+
|
|
58
|
+
if (!child) {
|
|
59
|
+
context.report({
|
|
60
|
+
node,
|
|
61
|
+
messageId: 'a11y-clickable-element-has-text',
|
|
62
|
+
data: {
|
|
63
|
+
message: 'a, button要素にはテキストを設定してください。要素内にアイコン、画像のみを設置する場合はSmartHR UIのvisuallyHiddenText、通常のHTML要素にはaltなどの代替テキスト用属性を指定してください',
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
module.exports.schema = []
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# smarthr/a11y-image-has-alt-attribute
|
|
2
|
+
|
|
3
|
+
- 画像やアイコンにalt属性を設定することを強制するルールです
|
|
4
|
+
|
|
5
|
+
## rules
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
{
|
|
9
|
+
rules: {
|
|
10
|
+
'smarthr/a11y-image-has-alt-attribute': 'error', // 'warn', 'off'
|
|
11
|
+
},
|
|
12
|
+
}
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## ❌ Incorrect
|
|
16
|
+
|
|
17
|
+
```jsx
|
|
18
|
+
<Img />
|
|
19
|
+
```
|
|
20
|
+
```jsx
|
|
21
|
+
<Image>
|
|
22
|
+
<Any />
|
|
23
|
+
</Image>
|
|
24
|
+
```
|
|
25
|
+
```jsx
|
|
26
|
+
<Icon />
|
|
27
|
+
```
|
|
28
|
+
```jsx
|
|
29
|
+
import styled from 'styled-components'
|
|
30
|
+
|
|
31
|
+
const StyledHoge = styled.img``
|
|
32
|
+
const StyledFuga = styled(Img)``
|
|
33
|
+
const StyledPiyo = styled(Icon)``
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## ✅ Correct
|
|
37
|
+
|
|
38
|
+
```jsx
|
|
39
|
+
<Img alt="message" />
|
|
40
|
+
```
|
|
41
|
+
```jsx
|
|
42
|
+
<Image alt="message">
|
|
43
|
+
<Any />
|
|
44
|
+
</Image>
|
|
45
|
+
```
|
|
46
|
+
```jsx
|
|
47
|
+
<Icon alt="message" />
|
|
48
|
+
```
|
|
49
|
+
```jsx
|
|
50
|
+
import styled from 'styled-components'
|
|
51
|
+
|
|
52
|
+
const StyledImage = styled.img``
|
|
53
|
+
const StyledImg = styled(Img)``
|
|
54
|
+
const StyledIcon = styled(Icon)``
|
|
55
|
+
```
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const { generateTagFormatter } = require('../../libs/format_styled_components')
|
|
2
|
+
|
|
3
|
+
const EXPECTED_NAMES = {
|
|
4
|
+
'Img$': 'Img$',
|
|
5
|
+
'Image$': 'Image$',
|
|
6
|
+
'Icon$': 'Icon$',
|
|
7
|
+
'^(img|svg)$': '(Img|Image|Icon)$',
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
module.exports = {
|
|
11
|
+
meta: {
|
|
12
|
+
type: 'suggestion',
|
|
13
|
+
messages: {
|
|
14
|
+
'format-styled-components': '{{ message }}',
|
|
15
|
+
'a11y-image-has-alt-attribute': '{{ message }}',
|
|
16
|
+
},
|
|
17
|
+
schema: [],
|
|
18
|
+
},
|
|
19
|
+
create(context) {
|
|
20
|
+
return {
|
|
21
|
+
...generateTagFormatter({ context, EXPECTED_NAMES }),
|
|
22
|
+
JSXOpeningElement: (node) => {
|
|
23
|
+
if ((node.name.name || '').match(/(img|image)$/i)) { // HINT: Iconは別途テキストが存在する場合が多いためチェックの対象外とする
|
|
24
|
+
const alt = node.attributes.find((a) => a.name.name === 'alt')
|
|
25
|
+
|
|
26
|
+
let message = ''
|
|
27
|
+
|
|
28
|
+
if (!alt) {
|
|
29
|
+
message = '画像にはalt属性を指定してください。SVG component の場合、altを属性として受け取れるようにした上で `<svg role="img" aria-label={alt}>` のように指定してください。画像ではない場合、img or image を末尾に持たない名称に変更してください。'
|
|
30
|
+
} else if (alt.value.value === '') {
|
|
31
|
+
message = '画像の情報をテキストにした代替テキスト(`alt`)を設定してください。装飾目的の画像など、alt属性に指定すべき文字がない場合は背景画像にすることを検討してください。'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (message) {
|
|
35
|
+
context.report({
|
|
36
|
+
node,
|
|
37
|
+
messageId: 'a11y-image-has-alt-attribute',
|
|
38
|
+
data: {
|
|
39
|
+
message,
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
module.exports.schema = []
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# smarthr/a11y-trigger-has-button
|
|
2
|
+
|
|
3
|
+
- DropdownTriggerやDialogTriggerの直下にButtonを設置することを強制するルールです
|
|
4
|
+
|
|
5
|
+
## rules
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
{
|
|
9
|
+
rules: {
|
|
10
|
+
'smarthr/a11y-trigger-has-button': 'error', // 'warn', 'off'
|
|
11
|
+
},
|
|
12
|
+
}
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## ❌ Incorrect
|
|
16
|
+
|
|
17
|
+
```jsx
|
|
18
|
+
<DropdownTrigger>
|
|
19
|
+
<Xxx />
|
|
20
|
+
</DropdownTrigger>
|
|
21
|
+
```
|
|
22
|
+
```jsx
|
|
23
|
+
<DialogTrigger>
|
|
24
|
+
<Yyy />
|
|
25
|
+
</DialogTrigger>
|
|
26
|
+
```
|
|
27
|
+
```jsx
|
|
28
|
+
import styled from 'styled-components'
|
|
29
|
+
|
|
30
|
+
const StyledHoge = styled.a``
|
|
31
|
+
const StyledFuga = styled.button``
|
|
32
|
+
const StyledAnchor = styled(Link)``
|
|
33
|
+
const StyledBtn = styled(Button)``
|
|
34
|
+
const StyledPiyo = styled(DropdownTrigger)``
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## ✅ Correct
|
|
38
|
+
|
|
39
|
+
```jsx
|
|
40
|
+
<DropdownTrigger>
|
|
41
|
+
<Button />
|
|
42
|
+
</DropdownTrigger>
|
|
43
|
+
```
|
|
44
|
+
```jsx
|
|
45
|
+
<DialogTrigger>
|
|
46
|
+
<XxxButton />
|
|
47
|
+
</DialogTrigger>
|
|
48
|
+
```
|
|
49
|
+
```jsx
|
|
50
|
+
import styled from 'styled-components'
|
|
51
|
+
|
|
52
|
+
const StyledAnchor = styled.a``
|
|
53
|
+
const StyledButton = styled.button``
|
|
54
|
+
const StyledLink = styled(Link)``
|
|
55
|
+
const StyledButton = styled(Button)``
|
|
56
|
+
const StyledDropdownTrigger = styled(DropdownTrigger)``
|
|
57
|
+
```
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const { generateTagFormatter } = require('../../libs/format_styled_components')
|
|
2
|
+
|
|
3
|
+
const EXPECTED_NAMES = {
|
|
4
|
+
'DropdownTrigger$': 'DropdownTrigger$',
|
|
5
|
+
'DialogTrigger$': 'DialogTrigger$',
|
|
6
|
+
'(b|B)utton$': 'Button$',
|
|
7
|
+
'AnchorButton$': 'AnchorButton$',
|
|
8
|
+
'ButtonAnchor$': 'ButtonAnchor$',
|
|
9
|
+
'Anchor$': 'Anchor$',
|
|
10
|
+
'Link$': 'Link$',
|
|
11
|
+
'^a$': '(Anchor|Link)$',
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const filterFalsyJSXText = (cs) => cs.filter((c) => (
|
|
15
|
+
!(c.type === 'JSXText' && c.value.match(/^\s*\n+\s*$/))
|
|
16
|
+
))
|
|
17
|
+
|
|
18
|
+
module.exports = {
|
|
19
|
+
meta: {
|
|
20
|
+
type: 'suggestion',
|
|
21
|
+
messages: {
|
|
22
|
+
'format-styled-components': '{{ message }}',
|
|
23
|
+
'a11y-trigger-has-button': '{{ message }}',
|
|
24
|
+
},
|
|
25
|
+
schema: [],
|
|
26
|
+
},
|
|
27
|
+
create(context) {
|
|
28
|
+
return {
|
|
29
|
+
...generateTagFormatter({ context, EXPECTED_NAMES }),
|
|
30
|
+
JSXElement: (parentNode) => {
|
|
31
|
+
// HINT: 閉じタグが存在しない === 子が存在しない
|
|
32
|
+
// 子を持っていない場合はおそらく固定の要素を吐き出すコンポーネントと考えられるため
|
|
33
|
+
// その中身をチェックすることで担保できるのでskipする
|
|
34
|
+
if (!parentNode.closingElement) {
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const node = parentNode.openingElement
|
|
39
|
+
|
|
40
|
+
if (!node.name.name) {
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const match = node.name.name.match(/(Dropdown|Dialog)Trigger$/)
|
|
45
|
+
|
|
46
|
+
if (!match || node.name.name.match(/HelpDialogTrigger$/)) {
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
filterFalsyJSXText(parentNode.children).forEach((c) => {
|
|
51
|
+
// `<DialogTrigger>{button}</DialogTrigger>` のような場合は許可する
|
|
52
|
+
if (c.type === 'JSXExpressionContainer') {
|
|
53
|
+
return false
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (
|
|
57
|
+
c.type !== 'JSXElement' ||
|
|
58
|
+
!c.openingElement.name.name.match(/(b|B)utton$/) ||
|
|
59
|
+
c.openingElement.name.name.match(/AnchorButton?/)
|
|
60
|
+
) {
|
|
61
|
+
context.report({
|
|
62
|
+
node: c,
|
|
63
|
+
messageId: 'a11y-trigger-has-button',
|
|
64
|
+
data: {
|
|
65
|
+
message: `${match[1]}Trigger の直下にはbuttonコンポーネントのみ設置してください`,
|
|
66
|
+
},
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
module.exports.schema = []
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# smarthr/best-practice-for-date
|
|
2
|
+
|
|
3
|
+
- `new Date(arg)` と `Date.parse(arg)` を禁止するルールです
|
|
4
|
+
- `new Date()` と`new Date(year, month, date)` などのように引数が1つ以外の場合は許容します
|
|
5
|
+
- これらはブラウザの実装によっては意図しない日付として解釈されてしまう問題を回避するためのものです
|
|
6
|
+
- [Date オブジェクトを生成するいくつかの方法](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Date#date_%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%82%92%E7%94%9F%E6%88%90%E3%81%99%E3%82%8B%E3%81%84%E3%81%8F%E3%81%A4%E3%81%8B%E3%81%AE%E6%96%B9%E6%B3%95)
|
|
7
|
+
- [Date.parse](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Date#date.parse)
|
|
8
|
+
|
|
9
|
+
## rules
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
{
|
|
13
|
+
rules: {
|
|
14
|
+
'smarthr/best-practice-for-date': 'error', // 'warn', 'off'
|
|
15
|
+
},
|
|
16
|
+
}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## ❌ Incorrect
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
new Date('2022/12/31')
|
|
23
|
+
|
|
24
|
+
new Date(arg)
|
|
25
|
+
|
|
26
|
+
Date.parse(value)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## ✅ Correct
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
new Date(2022, 11, 31)
|
|
34
|
+
|
|
35
|
+
const args = arg.split('/')
|
|
36
|
+
new Date(args[0], parseInt(args[1], 10) - 1, args[2])
|
|
37
|
+
|
|
38
|
+
// use dayjs
|
|
39
|
+
dayjs(arg)
|
|
40
|
+
```
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
meta: {
|
|
3
|
+
type: 'suggestion',
|
|
4
|
+
messages: {
|
|
5
|
+
'best-practice-for-date': '{{ message }}',
|
|
6
|
+
},
|
|
7
|
+
schema: [],
|
|
8
|
+
},
|
|
9
|
+
create(context) {
|
|
10
|
+
return {
|
|
11
|
+
NewExpression: (node) => {
|
|
12
|
+
if (
|
|
13
|
+
node.callee.name === 'Date' &&
|
|
14
|
+
node.arguments.length == 1
|
|
15
|
+
) {
|
|
16
|
+
context.report({
|
|
17
|
+
node,
|
|
18
|
+
messageId: 'best-practice-for-date',
|
|
19
|
+
data: {
|
|
20
|
+
message: "'new Date(arg)' のように引数一つのみの指定方は実行環境により結果が変わる可能性があるため 'new Date(2022, 12 - 1, 31)' のようにparseするなど他の方法を検討してください。",
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
CallExpression: (node) => {
|
|
26
|
+
if (
|
|
27
|
+
node.callee.object?.name === 'Date' &&
|
|
28
|
+
node.callee.property?.name === 'parse'
|
|
29
|
+
) {
|
|
30
|
+
context.report({
|
|
31
|
+
node,
|
|
32
|
+
messageId: 'best-practice-for-date',
|
|
33
|
+
data: {
|
|
34
|
+
message: 'Date.parse は日付形式の解釈がブラウザによって異なるため、他の手段を検討してください',
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
module.exports.schema = []
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# smarthr/format-import-path
|
|
2
|
+
|
|
3
|
+
- importする際のpathをフォーマットするruleです
|
|
4
|
+
- ディレクトリ構造からドメインを識別して相対パス、絶対パスいずれかにするかを判定することが出来ます
|
|
5
|
+
- 例: crews/index 以下同士でのimportは相対パス、crews/index外のファイルimportする場合は絶対パスにする
|
|
6
|
+
- eslint を `--fix` オプション付きで実行すると自動的にパスを置き換えます
|
|
7
|
+
|
|
8
|
+
## config
|
|
9
|
+
|
|
10
|
+
- tsconfig.json の compilerOptions.pathsに '@/*' としてroot path を指定する必要があります
|
|
11
|
+
- ドメインを識別するために以下の設定を記述する必要があります
|
|
12
|
+
- globalModuleDir
|
|
13
|
+
- 全体で利用するファイルを収めているディレクトリを相対パスで指定します
|
|
14
|
+
- domainModuleDir:
|
|
15
|
+
- ドメイン内で共通のファイルを収めているディレクトリ名を指定します
|
|
16
|
+
- domainConstituteDir
|
|
17
|
+
- ドメインを構築するディレクトリ名を指定します
|
|
18
|
+
|
|
19
|
+
### ディレクトリ例
|
|
20
|
+
```
|
|
21
|
+
/ constants
|
|
22
|
+
/ modules // 全体共通ディレクトリ
|
|
23
|
+
/ crews
|
|
24
|
+
/ modules // 共通ディレクトリ
|
|
25
|
+
/ views
|
|
26
|
+
/ parts
|
|
27
|
+
/ index
|
|
28
|
+
/ adapters
|
|
29
|
+
/ index.ts
|
|
30
|
+
/ hoge.ts
|
|
31
|
+
/ slices
|
|
32
|
+
/ index.ts
|
|
33
|
+
/ views
|
|
34
|
+
/ index.ts
|
|
35
|
+
/ parts
|
|
36
|
+
/ Abc.ts
|
|
37
|
+
/ show
|
|
38
|
+
/ views
|
|
39
|
+
/ parts
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 指定例
|
|
43
|
+
```
|
|
44
|
+
const DOMAIN_RULE_ARGS = {
|
|
45
|
+
globalModuleDir: [ './constants', './modules' ],
|
|
46
|
+
domainModuleDir: [ 'modules' ],
|
|
47
|
+
domainConstituteDir: [ 'adapters', 'slices', 'views', 'parts' ],
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## rules
|
|
52
|
+
|
|
53
|
+
```js
|
|
54
|
+
{
|
|
55
|
+
rules: {
|
|
56
|
+
'smarthr/format-import-path': [
|
|
57
|
+
'error', // 'warn', 'off'
|
|
58
|
+
{
|
|
59
|
+
...DOMAIN_RULE_ARGS,
|
|
60
|
+
format: {
|
|
61
|
+
// 'relative', 'auto', 'none'
|
|
62
|
+
// all: 'absolute',
|
|
63
|
+
outside: 'auto',
|
|
64
|
+
globalModule: 'absolute',
|
|
65
|
+
module: 'relative',
|
|
66
|
+
domain: 'relative',
|
|
67
|
+
lower: 'relative',
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## ❌ Incorrect
|
|
76
|
+
|
|
77
|
+
```js
|
|
78
|
+
// crews/index/views/index.js
|
|
79
|
+
|
|
80
|
+
import slice from '@/crews/index/slice'
|
|
81
|
+
import hoge from '@/crews/index/adapter/hoge'
|
|
82
|
+
import Abc from '@/crews/index/views/parts/Abc'
|
|
83
|
+
import modulePart from '@/crews/modules/views/part'
|
|
84
|
+
import showPart from '../../show/views/parts'
|
|
85
|
+
import globalModulePart from '../../../module/views/part'
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## ✅ Correct
|
|
89
|
+
|
|
90
|
+
```js
|
|
91
|
+
// crews/index/views/index.js
|
|
92
|
+
|
|
93
|
+
import slice from '../slice'
|
|
94
|
+
import hoge from '../adapter/hoge'
|
|
95
|
+
import Abc from './parts/Abc'
|
|
96
|
+
import modulePart from '../../modules/views/parts'
|
|
97
|
+
import showPart from '@/crews/show/views/parts'
|
|
98
|
+
import globalModulePart from '@/modules/views/parts'
|
|
99
|
+
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const path = require('path')
|
|
2
|
-
const { replacePaths } = require('
|
|
3
|
-
const { BASE_SCHEMA_PROPERTIES, calculateDomainContext, calculateDomainNode } = require('
|
|
2
|
+
const { replacePaths } = require('../../libs/common')
|
|
3
|
+
const { BASE_SCHEMA_PROPERTIES, calculateDomainContext, calculateDomainNode } = require('../../libs/common_domain')
|
|
4
4
|
|
|
5
5
|
const SCHEMA_FORMAT_PROPERTY = { type: 'string', pattern: '^(none|absolute|relative|auto)$', default: 'none'}
|
|
6
6
|
const SCHEMA = [
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# smarthr/format-translate-component
|
|
2
|
+
|
|
3
|
+
- 翻訳用コンポーネントを適用する際のルールを定めます
|
|
4
|
+
|
|
5
|
+
## rules
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
{
|
|
9
|
+
rules: {
|
|
10
|
+
'smarthr/format-translate-component': [
|
|
11
|
+
'error', // 'warn', 'off'
|
|
12
|
+
{
|
|
13
|
+
componentName: 'Translate',
|
|
14
|
+
// componentPath: '@/any/path/Translate',
|
|
15
|
+
// prohibitAttributies: ['data-translate'],
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## ❌ Incorrect
|
|
23
|
+
|
|
24
|
+
```jsx
|
|
25
|
+
<Translate><Any>ほげ</Any></Translate>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```jsx
|
|
29
|
+
<Translate><Any /></Translate>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```jsx
|
|
33
|
+
<Translate></Translate>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```jsx
|
|
37
|
+
// prohibitAttributies: ['data-translate'],
|
|
38
|
+
<Any data-translate="true">...</Any>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## ✅ Correct
|
|
42
|
+
|
|
43
|
+
```jsx
|
|
44
|
+
<Translate>ほげ</Translate>
|
|
45
|
+
```
|
|
46
|
+
```jsx
|
|
47
|
+
<Translate>ほげ<br />ふが</Translate>
|
|
48
|
+
```
|
|
49
|
+
```jsx
|
|
50
|
+
<Translate>{any}</Translate>
|
|
51
|
+
```
|
|
52
|
+
```jsx
|
|
53
|
+
<Translate dangerouslySetInnerHTML={{ __html: "ほげ" }} />
|
|
54
|
+
```
|
|
55
|
+
```jsx
|
|
56
|
+
// prohibitAttributies: ['data-translate'],
|
|
57
|
+
<Any data-hoge="true">...</Any>
|
|
58
|
+
```
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const SCHEMA = [
|
|
2
|
+
{
|
|
3
|
+
type: 'object',
|
|
4
|
+
required: [
|
|
5
|
+
'componentName',
|
|
6
|
+
],
|
|
7
|
+
properties: {
|
|
8
|
+
componentPath: { type: 'string', default: '' },
|
|
9
|
+
componentName: { type: 'string' },
|
|
10
|
+
prohibitAttributies: { type: 'array', items: { type: 'string' }, default: [] },
|
|
11
|
+
},
|
|
12
|
+
additionalProperties: false,
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
module.exports = {
|
|
17
|
+
meta: {
|
|
18
|
+
type: 'suggestion',
|
|
19
|
+
messages: {
|
|
20
|
+
'format-translate-component': '{{ message }}',
|
|
21
|
+
},
|
|
22
|
+
schema: SCHEMA,
|
|
23
|
+
},
|
|
24
|
+
create(context) {
|
|
25
|
+
const { componentPath, componentName, prohibitAttributies } = context.options[0]
|
|
26
|
+
let JSXAttribute = () => {}
|
|
27
|
+
|
|
28
|
+
if (prohibitAttributies) {
|
|
29
|
+
JSXAttribute = (node) => {
|
|
30
|
+
const hit = prohibitAttributies.find((a) => a === node.name.name)
|
|
31
|
+
|
|
32
|
+
if (hit) {
|
|
33
|
+
context.report({
|
|
34
|
+
node,
|
|
35
|
+
messageId: 'format-translate-component',
|
|
36
|
+
data: {
|
|
37
|
+
message: `${hit} 属性は使用せず、 ${componentPath || componentName} コンポーネントを利用してください`,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
JSXAttribute,
|
|
46
|
+
JSXOpeningElement: (node) => {
|
|
47
|
+
// HINT: 翻訳コンポーネントはテキストとbrのみ許容する
|
|
48
|
+
if (node.name.name === componentName) {
|
|
49
|
+
let existValidChild = false
|
|
50
|
+
let existNotBrElement = false
|
|
51
|
+
|
|
52
|
+
node.parent.children.forEach((c) => {
|
|
53
|
+
switch (c.type) {
|
|
54
|
+
case 'JSXText':
|
|
55
|
+
// HINT: 空白と改行のみの場合はテキストが存在する扱いにはしない
|
|
56
|
+
if (c.value.replace(/(\s|\n)+/g, '')) {
|
|
57
|
+
existValidChild = true
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
break
|
|
61
|
+
case 'JSXExpressionContainer':
|
|
62
|
+
// TODO 変数がstringのみか判定できるなら対応したい
|
|
63
|
+
existValidChild = true
|
|
64
|
+
|
|
65
|
+
break
|
|
66
|
+
case 'JSXElement':
|
|
67
|
+
if (c.openingElement.name.name !== 'br') {
|
|
68
|
+
existNotBrElement = true
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
break
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const message = (() => {
|
|
76
|
+
if (existNotBrElement) {
|
|
77
|
+
return `${componentName} 内では <br /> 以外のタグは使えません`
|
|
78
|
+
} else if (!existValidChild && !node.attributes.some((a) => a.name.name === 'dangerouslySetInnerHTML')) {
|
|
79
|
+
return `${componentName} 内には必ずテキストを設置してください`
|
|
80
|
+
}
|
|
81
|
+
})()
|
|
82
|
+
|
|
83
|
+
if (message) {
|
|
84
|
+
context.report({
|
|
85
|
+
node,
|
|
86
|
+
messageId: 'format-translate-component',
|
|
87
|
+
data: {
|
|
88
|
+
message,
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
module.exports.schema = SCHEMA
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# smarthr/jsx-start-with-spread-attributes
|
|
2
|
+
|
|
3
|
+
- jsxを記述する際、意図しない属性の上書きを防ぐため、spread-attributesを先に指定するように強制するruleです
|
|
4
|
+
- eslint を `--fix` オプション付きで実行する際、 fix option を true にすると自動修正します
|
|
5
|
+
|
|
6
|
+
## rules
|
|
7
|
+
|
|
8
|
+
```js
|
|
9
|
+
{
|
|
10
|
+
rules: {
|
|
11
|
+
'smarthr/jsx-start-with-spread-attributes': [
|
|
12
|
+
'error', // 'warn', 'off'
|
|
13
|
+
{
|
|
14
|
+
fix: false, // true
|
|
15
|
+
},
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## ❌ Incorrect
|
|
22
|
+
|
|
23
|
+
```jsx
|
|
24
|
+
<AnyComponent hoge="hoge" {...props} />
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## ✅ Correct
|
|
28
|
+
|
|
29
|
+
```jsx
|
|
30
|
+
<AnyComponent {...props} hoge="hoge" />
|
|
31
|
+
```
|
package/rules/{jsx-start-with-spread-attributes.js → jsx-start-with-spread-attributes/index.js}
RENAMED
|
File without changes
|