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.
Files changed (45) hide show
  1. package/.github/CODEOWNERS +3 -0
  2. package/CHANGELOG.md +27 -0
  3. package/README.md +15 -489
  4. package/github/CODEOWNERS +3 -0
  5. package/index.js +3 -6
  6. package/libs/format_styled_components.js +57 -0
  7. package/package.json +1 -1
  8. package/rules/a11y-clickable-element-has-text/README.md +61 -0
  9. package/rules/a11y-clickable-element-has-text/index.js +71 -0
  10. package/rules/a11y-image-has-alt-attribute/README.md +55 -0
  11. package/rules/a11y-image-has-alt-attribute/index.js +48 -0
  12. package/rules/a11y-trigger-has-button/README.md +57 -0
  13. package/rules/a11y-trigger-has-button/index.js +74 -0
  14. package/rules/best-practice-for-date/README.md +40 -0
  15. package/rules/best-practice-for-date/index.js +42 -0
  16. package/rules/format-import-path/README.md +99 -0
  17. package/rules/{format-import-path.js → format-import-path/index.js} +2 -2
  18. package/rules/format-translate-component/README.md +58 -0
  19. package/rules/format-translate-component/index.js +97 -0
  20. package/rules/jsx-start-with-spread-attributes/README.md +31 -0
  21. package/rules/{jsx-start-with-spread-attributes.js → jsx-start-with-spread-attributes/index.js} +0 -0
  22. package/rules/no-import-other-domain/README.md +85 -0
  23. package/rules/{no-import-other-domain.js → no-import-other-domain/index.js} +2 -2
  24. package/rules/prohibit-export-array-type/README.md +28 -0
  25. package/rules/prohibit-export-array-type/index.js +28 -0
  26. package/rules/prohibit-file-name/README.md +35 -0
  27. package/rules/prohibit-file-name/index.js +61 -0
  28. package/rules/prohibit-import/README.md +44 -0
  29. package/rules/{prohibit-import.js → prohibit-import/index.js} +0 -0
  30. package/rules/redundant-name/README.md +94 -0
  31. package/rules/{redundant-name.js → redundant-name/index.js} +15 -5
  32. package/rules/require-barrel-import/README.md +39 -0
  33. package/rules/{require-barrel-import.js → require-barrel-import/index.js} +1 -1
  34. package/rules/require-export/README.md +43 -0
  35. package/rules/require-export/index.js +90 -0
  36. package/rules/require-import/README.md +51 -0
  37. package/rules/{require-import.js → require-import/index.js} +0 -0
  38. package/test/a11y-clickable-element-has-text.js +142 -0
  39. package/test/a11y-image-has-alt-attribute.js +44 -0
  40. package/test/a11y-trigger-has-button.js +50 -0
  41. package/test/best-practice-for-date.js +31 -0
  42. package/test/format-translate-component.js +37 -0
  43. package/test/prohibit-file-name.js +45 -0
  44. package/test/require-export.js +83 -0
  45. package/rules/a11y-icon-button-has-name.js +0 -56
@@ -0,0 +1,85 @@
1
+ # smarthr/no-import-other-domain
2
+
3
+ - ドメイン外からのimportを防ぐruleです
4
+ - 例: crews/index 以下からのimportはOK, crews/index から crews/show 以下をimportするとNG
5
+ - ディレクトリ構造からドメインを識別して判定することが出来ます
6
+
7
+ ## config
8
+
9
+ - tsconfig.json の compilerOptions.pathsに '@/*' としてroot path を指定する必要があります
10
+ - ドメインを識別するために以下の設定を記述する必要があります
11
+ - globalModuleDir
12
+ - 全体で利用するファイルを収めているディレクトリを相対パスで指定します
13
+ - domainModuleDir:
14
+ - ドメイン内で共通のファイルを収めているディレクトリ名を指定します
15
+ - domainConstituteDir
16
+ - ドメインを構築するディレクトリ名を指定します
17
+
18
+ ### ディレクトリ例
19
+ ```
20
+ / constants
21
+ / modules // 全体共通ディレクトリ
22
+ / crews
23
+ / modules // 共通ディレクトリ
24
+ / views
25
+ / parts
26
+ / index
27
+ / adapters
28
+ / index.ts
29
+ / hoge.ts
30
+ / slices
31
+ / index.ts
32
+ / views
33
+ / index.ts
34
+ / parts
35
+ / Abc.ts
36
+ / show
37
+ / views
38
+ / parts
39
+ ```
40
+
41
+ ### 指定例
42
+ ```
43
+ const DOMAIN_RULE_ARGS = {
44
+ globalModuleDir: [ './constants', './modules' ],
45
+ domainModuleDir: [ 'modules' ],
46
+ domainConstituteDir: [ 'adapters', 'slices', 'views', 'parts' ],
47
+ }
48
+ ```
49
+
50
+ ## rules
51
+
52
+ ```js
53
+ {
54
+ rules: {
55
+ 'smarthr/no-import-other-domain': [
56
+ 'error', // 'warn', 'off'
57
+ {
58
+ ...DOMAIN_RULE_ARGS,
59
+ // analyticsMode: 'all', // 'same-domain', 'another-domain'
60
+ }
61
+ ]
62
+ },
63
+ }
64
+ ```
65
+
66
+ ## ❌ Incorrect
67
+
68
+ ```js
69
+ // crews/index/views/index.js
70
+
71
+ import showPart1 from '@/crews/show/views/parts'
72
+ import showPart2 from '../../show/views/parts'
73
+ ```
74
+
75
+ ## ✅ Correct
76
+
77
+ ```js
78
+ // crews/index/views/index.js
79
+
80
+ import slice from '../slice'
81
+ import hoge from '../adapter/hoge'
82
+ import Abc from './parts/Abc'
83
+ import modulePart from '../../modules/views/parts'
84
+ import globalModulePart from '@/modules/views/parts'
85
+ ```
@@ -1,7 +1,7 @@
1
1
  const path = require('path')
2
2
  const fs = require('fs')
3
- const { replacePaths, rootPath } = require('../libs/common')
4
- const { BASE_SCHEMA_PROPERTIES, calculateDomainContext, calculateDomainNode } = require('../libs/common_domain')
3
+ const { replacePaths, rootPath } = require('../../libs/common')
4
+ const { BASE_SCHEMA_PROPERTIES, calculateDomainContext, calculateDomainNode } = require('../../libs/common_domain')
5
5
 
6
6
  const SCHEMA = [
7
7
  {
@@ -0,0 +1,28 @@
1
+ # smarthr/prohibit-export-array-type
2
+
3
+ - 配列の型をexport出来ないように制御するルールです
4
+ - 利用するファイルで `ItemProps[]` のように配列指定を強制する目的などで利用できます
5
+
6
+ ## rules
7
+
8
+ ```js
9
+ {
10
+ rules: {
11
+ 'smarthr/prohibit-export-array-type': 'error', // 'warn', 'off'
12
+ },
13
+ }
14
+ ```
15
+
16
+ ## ❌ Incorrect
17
+
18
+ ```js
19
+ type Item = { attr: string }
20
+ export type Items = Item[]
21
+ ```
22
+
23
+ ## ✅ Correct
24
+
25
+
26
+ ```js
27
+ export type Item = { attr: string }
28
+ ```
@@ -0,0 +1,28 @@
1
+ module.exports = {
2
+ meta: {
3
+ type: 'suggestion',
4
+ messages: {
5
+ 'prohibit-export-array-type': '{{ message }}',
6
+ },
7
+ schema: [],
8
+ },
9
+ create(context) {
10
+ const checker = (node) => {
11
+ if (node.declaration?.typeAnnotation?.type === 'TSArrayType') {
12
+ context.report({
13
+ node,
14
+ messageId: 'prohibit-export-array-type',
15
+ data: {
16
+ message: '利用する際、配列かどうかわかりにくいため、配列ではない状態でexportしてください',
17
+ },
18
+ })
19
+ }
20
+ }
21
+
22
+ return {
23
+ ExportDefaultDeclaration: checker,
24
+ ExportNamedDeclaration: checker,
25
+ }
26
+ },
27
+ }
28
+ module.exports.schema = []
@@ -0,0 +1,35 @@
1
+ # smarthr/prohibit-file-name
2
+
3
+ - 正規表現に合致するファイル、ディレクトリの作成を阻害するルールです
4
+
5
+ ## rules
6
+
7
+ ```js
8
+ {
9
+ rules: {
10
+ 'smarthr/prohibit-file-name': [
11
+ 'error', // 'warn', 'off'
12
+ {
13
+ '\/(actions|reducers)\/$': 'slicesディレクトリ内でcreateSliceを利用してください',
14
+ '\/views\/(page|template)\.(ts(x)?)$': 'index.$2、もしくはTemplate.$2にリネームしてください',
15
+ '\/modules\/(adapters|entities|repositories|slices)\/index\.ts(x)?$': '利用目的が推測出来ない為、リネームしてください(例: new, edit用ならform.tsなど)',
16
+ },
17
+ ]
18
+ },
19
+ }
20
+ ```
21
+
22
+ ## ❌ Incorrect
23
+
24
+ ```js
25
+ // src/pages/actions/index.ts
26
+ // src/modules/entities/index.ts
27
+ ```
28
+
29
+ ## ✅ Correct
30
+
31
+
32
+ ```js
33
+ // src/pages/slices/index.ts
34
+ // src/modules/entities/item.ts
35
+ ```
@@ -0,0 +1,61 @@
1
+ const SCHEMA = [{
2
+ type: 'object',
3
+ patternProperties: {
4
+ '.+': {
5
+ type: 'string',
6
+ },
7
+ },
8
+ additionalProperties: true,
9
+ }]
10
+
11
+
12
+ module.exports = {
13
+ meta: {
14
+ type: 'suggestion',
15
+ messages: {
16
+ 'prohibit-file-name': '{{ message }}',
17
+ },
18
+ schema: SCHEMA,
19
+ },
20
+ create(context) {
21
+ const options = context.options[0]
22
+ const filename = context.getFilename()
23
+ const targetPaths = Object.keys(options).filter((regex) => !!filename.match(new RegExp(regex)))
24
+
25
+
26
+ if (targetPaths.length === 0) {
27
+ return {}
28
+ }
29
+
30
+ const messages = []
31
+
32
+ targetPaths.forEach((path) => {
33
+ const message = options[path]
34
+
35
+ matcher = filename.match(new RegExp(path))
36
+
37
+ if (matcher) {
38
+ messages.push([...matcher].reduce(((prev, k, index) => prev.replaceAll(`\$${index}`, k)), message))
39
+ }
40
+ })
41
+
42
+ if (messages.length === 0) {
43
+ return {}
44
+ }
45
+
46
+ return {
47
+ Program: (node) => {
48
+ messages.forEach((message) => {
49
+ context.report({
50
+ node,
51
+ messageId: 'prohibit-file-name',
52
+ data: {
53
+ message,
54
+ },
55
+ })
56
+ })
57
+ },
58
+ }
59
+ },
60
+ }
61
+ module.exports.schema = SCHEMA
@@ -0,0 +1,44 @@
1
+ # smarthr/prohibit-import
2
+
3
+ - 例えば特定の module にバグが発見されたなど、importさせたくない場合に利用するルールです
4
+
5
+ ## rules
6
+
7
+ ```js
8
+ {
9
+ rules: {
10
+ 'smarthr/prohibit-import': [
11
+ 'error', // 'warn', 'off'
12
+ {
13
+ '^.+$': {
14
+ 'smarthr-ui': {
15
+ imported: ['SecondaryButtonAnchor'],
16
+ reportMessage: `{{module}}/{{export}} はXxxxxxなので利用せず yyyy/zzzz を利用してください`
17
+ },
18
+ }
19
+ '\/pages\/views\/': {
20
+ 'query-string': {
21
+ imported: true,
22
+ },
23
+ },
24
+ }
25
+ ]
26
+ },
27
+ }
28
+ ```
29
+
30
+ ## ❌ Incorrect
31
+
32
+ ```js
33
+ // src/pages/views/Page.tsx
34
+ import queryString from 'query-string'
35
+ import { SecondaryButtonAnchor } from 'smarthr-ui'
36
+ ```
37
+
38
+ ## ✅ Correct
39
+
40
+
41
+ ```js
42
+ // src/pages/views/Page.tsx
43
+ import { PrimaryButton, SecondaryButton } from 'smarthr-ui'
44
+ ```
@@ -0,0 +1,94 @@
1
+ # smarthr/redundant-name
2
+
3
+ - ファイル、コードの冗長な部分を取り除くことを提案するruleです
4
+ - ファイルが設置されているディレクトリ構造からキーワードを生成し、取り除く文字列を生成します
5
+
6
+ ## config
7
+
8
+ - tsconfig.json の compilerOptions.pathsに '@/*' としてroot path を指定する必要があります
9
+ - 以下の設定を行えます。全て省略可能です。
10
+ - ignoreKeywords
11
+ - ディレクトリ名から生成されるキーワードに含めたくない文字列を指定します
12
+ - betterNames
13
+ - 対象の名前を修正する候補を指定します
14
+ - allowedNames
15
+ - 許可する名前を指定します
16
+ - suffix:
17
+ - type のみ指定出来ます
18
+ - type のsuffixを指定します
19
+
20
+ ### ファイル例
21
+ - `@/crews/index/views/page.tsx` の場合
22
+ - 生成されるキーワードは `['crews', 'crew', 'index', 'page']`
23
+ - `@/crews/index/views/parts/Abc.tsx` の場合
24
+ - 生成されるキーワードは `['crews', 'crew', 'index', 'Abc']`
25
+ - `@/crews/index/repositories/index.ts` の場合
26
+ - 生成されるキーワードは `['crews', 'crew', 'index', 'repositories', 'repository']`
27
+
28
+
29
+ ## rules
30
+
31
+ ```js
32
+ const ignorekeywords = ['views', 'parts']
33
+ const betterNames = {
34
+ '\/repositories\/': {
35
+ operator: '-',
36
+ names: ['repository', 'Repository'],
37
+ },
38
+ '\/entities\/': {
39
+ operator: '+',
40
+ names: ['entity'],
41
+ },
42
+ '\/slices\/': {
43
+ operator: '=',
44
+ names: ['index'],
45
+ },
46
+ }
47
+ // const allowedNames = {
48
+ // '\/views\/crews\/histories\/': ['crewId'],
49
+ // }
50
+
51
+ {
52
+ rules: {
53
+ 'smarthr/redundant-name': [
54
+ 'error', // 'warn', 'off'
55
+ {
56
+ type: { ignorekeywords, suffix: ['Props', 'Type'] },
57
+ file: { ignorekeywords, betternames },
58
+ // property: { ignorekeywords, allowedNames },
59
+ // function: { ignorekeywords },
60
+ // variable: { ignorekeywords },
61
+ // class: { ignorekeywords },
62
+ }
63
+ ]
64
+ },
65
+ }
66
+ ```
67
+
68
+ ## ❌ Incorrect
69
+
70
+ ```js
71
+ // @/crews/index/views/page.tsx
72
+
73
+ type CrewIndexPage = { hoge: string }
74
+ type CrewsView = { hoge: string }
75
+ ```
76
+ ```js
77
+ // @/crews/show/repositories/index.tsx
78
+
79
+ type CrewIndexRepository = { hoge: () => any }
80
+ ```
81
+
82
+ ## ✅ Correct
83
+
84
+ ```js
85
+ // @/crews/index/views/page.tsx
86
+
87
+ type ItemProps = { hoge: string }
88
+ ```
89
+ ```js
90
+ // @/crews/show/repositories/index.tsx
91
+
92
+ type IndexProps = { hoge: () => any }
93
+ type ResponseType = { hoge: () => any }
94
+ ```
@@ -1,7 +1,7 @@
1
1
  const path = require('path')
2
2
  const Inflector = require('inflected')
3
3
 
4
- const { rootPath } = require('../libs/common')
4
+ const { rootPath } = require('../../libs/common')
5
5
 
6
6
  const uniq = (array) => array.filter((elem, index, self) => self.indexOf(elem) === index)
7
7
 
@@ -48,6 +48,10 @@ const DEFAULT_SCHEMA_PROPERTY = {
48
48
  },
49
49
  },
50
50
  },
51
+ allowedNames: {
52
+ type: 'array',
53
+ items: 'string',
54
+ },
51
55
  }
52
56
 
53
57
  const SCHEMA = [
@@ -91,9 +95,11 @@ const generateRedundantKeywords = ({ args, key, terminalImportName }) => {
91
95
  return prev
92
96
  }
93
97
 
94
- const singularized = Inflector.singularize(keyword)
95
-
96
- return singularized === keyword ? [...prev, keyword] : [...prev, keyword, singularized]
98
+ return [...prev, ...uniq([
99
+ Inflector.pluralize(keyword),
100
+ keyword,
101
+ Inflector.singularize(keyword),
102
+ ])]
97
103
  }, [])
98
104
  }
99
105
  const handleReportBetterName = ({
@@ -113,7 +119,11 @@ const handleReportBetterName = ({
113
119
  return (node) => {
114
120
  const name = fetchName(node)
115
121
 
116
- if (!name) {
122
+ if (
123
+ !name ||
124
+ option.allowedNames &&
125
+ Object.entries(option.allowedNames).find(([regex, calcs]) => filename.match(new RegExp(regex)) && calcs.find((c) => c === name))
126
+ ) {
117
127
  return
118
128
  }
119
129
 
@@ -0,0 +1,39 @@
1
+ # smarthr/require-barrel-import
2
+
3
+ - tsconfig.json の compilerOptions.pathsに '@/*' としてroot path を指定する必要があります
4
+ - importした対象が本来exportされているべきであるbarrel(index.tsなど)が有る場合、import pathの変更を促します
5
+ - 例: Page/parts/Menu/Item の import は Page/parts/Menu から行わせたい
6
+ - ディレクトリ内のindexファイルを捜査し、対象を決定します
7
+
8
+ ## rules
9
+
10
+ ```js
11
+ {
12
+ rules: {
13
+ 'smarthr/require-barrel-import': 'error',
14
+ },
15
+ }
16
+ ```
17
+
18
+ ## ❌ Incorrect
19
+
20
+ ```js
21
+ // client/src/views/Page/parts/Menu/index.ts
22
+ export { Menu } from './Menu'
23
+ export { Item } from './Item'
24
+
25
+ // client/src/App.tsx
26
+ import { Item } from './Page/parts/Menu/Item'
27
+ ```
28
+
29
+ ## ✅ Correct
30
+
31
+
32
+ ```js
33
+ // client/src/views/Page/parts/Menu/index.ts
34
+ export { Menu } from './Menu'
35
+ export { Item } from './Item'
36
+
37
+ // client/src/App.tsx
38
+ import { Item } from './Page/parts/Menu'
39
+ ```
@@ -1,6 +1,6 @@
1
1
  const path = require('path')
2
2
  const fs = require('fs')
3
- const { replacePaths, rootPath } = require('../libs/common')
3
+ const { replacePaths, rootPath } = require('../../libs/common')
4
4
  const calculateAbsoluteImportPath = (source) => {
5
5
  if (source[0] === '/') {
6
6
  return source
@@ -0,0 +1,43 @@
1
+ # smarthr/require-export
2
+
3
+ - 対象ファイルにexportを強制させたい場合に利用します
4
+ - 例: Page.tsx ではページタイトルを設定させたいので useTitle を必ずexportさせたい
5
+
6
+ ## rules
7
+
8
+ ```js
9
+ {
10
+ rules: {
11
+ 'smarthr/require-export': [
12
+ 'error',
13
+ {
14
+ 'adapter\/.+\.ts': ['Props', 'generator'],
15
+ // slice以下のファイルかつmodulesディレクトリに属さずファイル名にmockを含まないもの
16
+ '^(?=.*\/slices\/[a-zA-Z0-9]+\.ts)(?!.*(\/modules\/|mock\.)).*$': [ 'default' ],
17
+ },
18
+ ]
19
+ },
20
+ }
21
+ ```
22
+
23
+ ## ❌ Incorrect
24
+
25
+ ```js
26
+ // adapter/index.ts
27
+ export type Type = { abc: string }
28
+
29
+ // slice/index.ts
30
+ export const slice = { method: () => 'any action' }
31
+ ```
32
+
33
+ ## ✅ Correct
34
+
35
+
36
+ ```js
37
+ // adapter/index.ts
38
+ export type Props = { abc: string }
39
+
40
+ // slice/index.ts
41
+ const slice = { method: () => 'any action' }
42
+ export default slice
43
+ ```
@@ -0,0 +1,90 @@
1
+ const SCHEMA = [{
2
+ type: 'object',
3
+ patternProperties: {
4
+ '.+': {
5
+ type: 'array',
6
+ items: { type: 'string' },
7
+ additionalProperties: true,
8
+ },
9
+ },
10
+ additionalProperties: true,
11
+ }]
12
+
13
+ const fetchEdgeDeclaration = (node) => {
14
+ const { declaration } = node
15
+
16
+ if (!declaration) {
17
+ return node
18
+ }
19
+
20
+ return fetchEdgeDeclaration(declaration)
21
+ }
22
+
23
+ module.exports = {
24
+ meta: {
25
+ type: 'suggestion',
26
+ messages: {
27
+ 'require-export': '{{ message }}',
28
+ },
29
+ schema: SCHEMA,
30
+ },
31
+ create(context) {
32
+ const options = context.options[0]
33
+ const filename = context.getFilename()
34
+ const targetPathRegexs = Object.keys(options)
35
+ const targetRequires = targetPathRegexs.filter((regex) => !!filename.match(new RegExp(regex)))
36
+
37
+ if (targetRequires.length === 0) {
38
+ return {}
39
+ }
40
+
41
+ return {
42
+ Program: (node) => {
43
+ targetRequires.forEach((requireKey) => {
44
+ const option = options[requireKey]
45
+ let existDefault = false
46
+ const exports =
47
+ node.body
48
+ .filter((i) => {
49
+ if (i.type == 'ExportDefaultDeclaration') {
50
+ existDefault = true
51
+
52
+ return false
53
+ }
54
+
55
+ return i.type == 'ExportNamedDeclaration'
56
+ })
57
+ .map((i) => {
58
+ const declaration = fetchEdgeDeclaration(i)
59
+
60
+ if (declaration.id) {
61
+ return declaration.id.name
62
+ }
63
+ if (declaration.specifiers) {
64
+ return declaration.specifiers.map((s) => s.exported.name)
65
+ }
66
+ if (declaration.declarations) {
67
+ return declaration.declarations.map((d) => d.id.name || d.id.properties.map((p) => p.key.name))
68
+ }
69
+
70
+ return declaration
71
+ })
72
+ .flat(2)
73
+
74
+ let notExistsExports = [...(!existDefault && option.includes('default') ? ['default'] : []), ...option.filter((o) => o !== 'default' && !exports.includes(o))]
75
+
76
+ if (notExistsExports.length) {
77
+ context.report({
78
+ node,
79
+ messageId: 'require-export',
80
+ data: {
81
+ message: `${notExistsExports.join(', ')} をexportしてください`,
82
+ },
83
+ })
84
+ }
85
+ })
86
+ },
87
+ }
88
+ },
89
+ }
90
+ module.exports.schema = SCHEMA
@@ -0,0 +1,51 @@
1
+ # smarthr/require-import
2
+
3
+ - 対象ファイルにimportを強制させたい場合に利用します
4
+ - 例: Page.tsx ではページタイトルを設定させたいので useTitle を必ずimportさせたい
5
+
6
+ ## rules
7
+
8
+ ```js
9
+ {
10
+ rules: {
11
+ 'smarthr/require-import': [
12
+ 'error',
13
+ {
14
+ 'Buttons\/.+\.tsx': {
15
+ 'smarthr-ui': {
16
+ imported: ['SecondaryButton'],
17
+ reportMessage: 'Buttons以下のコンポーネントでは {{module}}/{{export}} を拡張するようにしてください',
18
+ },
19
+ },
20
+ 'Page.tsx$': {
21
+ './client/src/hooks/useTitle': {
22
+ imported: true,
23
+ reportMessage: '{{module}} を利用してください(ページタイトルを設定するため必要です)',
24
+ },
25
+ },
26
+ },
27
+ ]
28
+ },
29
+ }
30
+ ```
31
+
32
+ ## ❌ Incorrect
33
+
34
+ ```js
35
+ // client/src/Buttons/SecondaryButton.tsx
36
+ import { SecondaryButtonAnchor } from 'smarthr-ui'
37
+
38
+ // client/src/Page.tsx
39
+ import { SecondaryButton } from 'smarthr-ui'
40
+ ```
41
+
42
+ ## ✅ Correct
43
+
44
+
45
+ ```js
46
+ // client/src/Buttons/SecondaryButton.tsx
47
+ import { SecondaryButton } from 'smarthr-ui'
48
+
49
+ // client/src/Page.tsx
50
+ import useTitle from '.hooks/useTitle'
51
+ ```