eslint-plugin-react-suspense-check 1.0.0
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/README.ko.md +124 -0
- package/README.md +95 -0
- package/index.js +33 -0
- package/package.json +43 -0
- package/rules/detect-suspense-hook.js +119 -0
package/README.ko.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
### π°π· νκ΅μ΄
|
|
2
|
+
|
|
3
|
+
νκ΅ κ°λ°μλ€μ μν μ€λͺ
μμ
λλ€.
|
|
4
|
+
|
|
5
|
+
# eslint-plugin-react-suspense-check π°π·
|
|
6
|
+
|
|
7
|
+
> **React Suspense** λ°νμ μλ¬λ₯Ό λ€μ΄λ° κ·μΉ(Naming Convention)μΌλ‘ μλ°©νμΈμ.
|
|
8
|
+
|
|
9
|
+
[πΊπΈ English Docs](./README.md)
|
|
10
|
+
|
|
11
|
+
## π§ μ νμνκ°μ?
|
|
12
|
+
|
|
13
|
+
React Suspenseλ κ°λ ₯νμ§λ§, λ°μ΄ν° λ‘λ© μ€μΈ μ»΄ν¬λνΈλ₯Ό μμ `<Suspense>`λ‘ κ°μΈμ§ μμΌλ©΄ **μ± μ μ²΄κ° λ©μΆκ±°λ νμ νλ©΄**μ΄ λ μ μμ΅λλ€.
|
|
14
|
+
|
|
15
|
+
ESLintλ νμΌ κ±΄λνΈμ μμ νΈλ¦¬μ `<Suspense>`κ° μλμ§ νμΈν μ μμ΅λλ€. κ·Έλμ μ΄ νλ¬κ·ΈμΈμ **μ΄λ¦ μ§κΈ° κ·μΉ**μ κ°μ νμ¬ κ°λ°μκ° μ€μλ₯Ό μΈμ§νλλ‘ λμ΅λλ€.
|
|
16
|
+
|
|
17
|
+
1. **ν
(Hook)**: λ΄λΆμμ Suspenseλ₯Ό μ λ°(Promise throw)νλ ν
μ λ°λμ **`useSuspense...`** λ‘ μμν΄μΌ ν©λλ€.
|
|
18
|
+
2. **μ»΄ν¬λνΈ**: μ ν
μ μ¬μ©νλ μ»΄ν¬λνΈλ λ°λμ **`Suspense...`** λ‘ μμν΄μΌ ν©λλ€.
|
|
19
|
+
|
|
20
|
+
μ¦, μ»΄ν¬λνΈ μ΄λ¦λ§ λ΄λ _"μ, μ΄κ±΄ μΈ λ Suspenseλ‘ κ°μΈμ€μΌ νλꡬλ!"_ λΌκ³ μ μ μκ² λ§λλ κ²μ
λλ€.
|
|
21
|
+
|
|
22
|
+
## π¦ μ€μΉ
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# npm
|
|
26
|
+
npm install --save-dev eslint-plugin-react-suspense-check
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# pnpm
|
|
31
|
+
pnpm add -D eslint-plugin-react-suspense-check
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# yarn
|
|
36
|
+
yarn add -D eslint-plugin-react-suspense-check
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### βοΈ μ€μ λ°©λ²
|
|
40
|
+
|
|
41
|
+
eslint.config.mjs (λλ .js) νμΌμ μλ λ΄μ©μ μΆκ°νμΈμ.
|
|
42
|
+
|
|
43
|
+
```JavaScript
|
|
44
|
+
|
|
45
|
+
import suspensePlugin from "eslint-plugin-react-suspense-check";
|
|
46
|
+
|
|
47
|
+
export default defineConfig([
|
|
48
|
+
// ... λ€λ₯Έ μ€μ λ€
|
|
49
|
+
...suspensePlugin.configs.recommended,// μΆμ² μ€μ μ μ©
|
|
50
|
+
|
|
51
|
+
//... λ€λ₯Έ μ€μ λ€
|
|
52
|
+
]);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
or
|
|
56
|
+
|
|
57
|
+
```JavaScript
|
|
58
|
+
|
|
59
|
+
import suspensePlugin from "eslint-plugin-react-suspense-check";
|
|
60
|
+
|
|
61
|
+
export default defineConfig([
|
|
62
|
+
// ... λ€λ₯Έ μ€μ λ€
|
|
63
|
+
|
|
64
|
+
extends: [
|
|
65
|
+
suspensePlugin.configs.recommended, // μΆμ² μ€μ μ μ©
|
|
66
|
+
],
|
|
67
|
+
//... λ€λ₯Έ μ€μ λ€
|
|
68
|
+
]);
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### π κ·μΉ μ€λͺ
|
|
72
|
+
|
|
73
|
+
`Suspense` λ₯Ό μ λ°νλ ν
κ³Ό μ»΄ν¬λνΈμ μ΄λ¦μ κ²μ¬ν©λλ€.
|
|
74
|
+
|
|
75
|
+
β μλͺ»λ μμ
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
// 1. ν
μ΄λ¦ μλ°
|
|
79
|
+
// Suspenseλ₯Ό μ λ°νλλ° μΌλ° ν
μ²λΌ μ΄λ¦μ μ§μ
|
|
80
|
+
function useUserData() {
|
|
81
|
+
throw promise;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 2. μ»΄ν¬λνΈ μ΄λ¦ μλ°
|
|
85
|
+
// Suspense ν
μ μ¬μ©νλλ° μΌλ° μ»΄ν¬λνΈμ²λΌ μ΄λ¦μ μ§μ
|
|
86
|
+
function UserProfile() {
|
|
87
|
+
const data = useSuspenseUser(); // <--- β οΈ κ²½κ³ : μ»΄ν¬λνΈ μ΄λ¦μ 'SuspenseUserProfile'λ‘ λ³κ²½νμΈμ.
|
|
88
|
+
return <div>{data.name}</div>;
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
β
μ¬λ°λ₯Έ μμ
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
// 1. ν
μ΄λ¦μ΄ 'useSuspense'λ‘ μμν¨
|
|
96
|
+
function useSuspenseUserData() {
|
|
97
|
+
throw promise;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 2. μ»΄ν¬λνΈ μ΄λ¦μ΄ 'Suspense'λ‘ μμν¨
|
|
101
|
+
// νΈμΆνλ μ¬λμ΄ <Suspense>κ° νμνλ€λ κ²μ μ΄λ¦λ§ λ³΄κ³ μ μ μμ.
|
|
102
|
+
function SuspenseUserProfile() {
|
|
103
|
+
const data = useSuspenseUserData();
|
|
104
|
+
return <div>{data.name}</div>;
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### π μΈμ΄ μ€μ (νκΈ μ§μ)
|
|
109
|
+
|
|
110
|
+
μλ¬ λ©μμ§λ₯Ό νκ΅μ΄λ‘ λ³΄κ³ μΆλ€λ©΄ μ΅μ
μ μ€μ ν μ μμ΅λλ€. (κΈ°λ³Έκ°μ μμ΄)
|
|
111
|
+
|
|
112
|
+
νκ΅μ΄ μ€μ μμ:
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
// eslint.config.mjs
|
|
116
|
+
export default [
|
|
117
|
+
{
|
|
118
|
+
//...
|
|
119
|
+
rules: {
|
|
120
|
+
'react-suspense-check/detect-suspense-hook': ['warn', { language: 'kr' }],
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
];
|
|
124
|
+
```
|
package/README.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
### π§ Why is this necessary?
|
|
2
|
+
|
|
3
|
+
React Suspense is powerful, but failing to wrap a component that is loading data in a parent `<Suspense>` boundary can cause the entire app to freeze or result in a white screen.
|
|
4
|
+
|
|
5
|
+
ESLint cannot verify if a `<Suspense>` boundary exists in the parent tree across different files. Therefore, this plugin **enforces naming conventions** to help developers recognize potential mistakes.
|
|
6
|
+
|
|
7
|
+
**Hook**: Hooks that trigger Suspense (throw a Promise) internally must start with useSuspense....
|
|
8
|
+
|
|
9
|
+
**Component**: Components using the above hooks must start with Suspense....
|
|
10
|
+
|
|
11
|
+
In short, it ensures that just by looking at the component name, you realize, "Ah, I need to wrap this in Suspense when using it!"
|
|
12
|
+
|
|
13
|
+
### π¦ Installation
|
|
14
|
+
|
|
15
|
+
```Bash
|
|
16
|
+
# npm
|
|
17
|
+
npm install --save-dev eslint-plugin-react-suspense-check
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```Bash
|
|
21
|
+
# pnpm
|
|
22
|
+
pnpm add -D eslint-plugin-react-suspense-check
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```Bash
|
|
26
|
+
# yarn
|
|
27
|
+
yarn add -D eslint-plugin-react-suspense-check
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### βοΈ Configuration
|
|
31
|
+
|
|
32
|
+
Add the following to your eslint.config.mjs (or .js) file.
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
import suspensePlugin from 'eslint-plugin-react-suspense-check';
|
|
36
|
+
|
|
37
|
+
export default defineConfig([
|
|
38
|
+
// ... other configs
|
|
39
|
+
...suspensePlugin.configs.recommended, // Apply recommended config
|
|
40
|
+
|
|
41
|
+
//... other configs
|
|
42
|
+
]);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
or
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
import suspensePlugin from "eslint-plugin-react-suspense-check";
|
|
49
|
+
|
|
50
|
+
export default defineConfig([
|
|
51
|
+
// ... other configs
|
|
52
|
+
|
|
53
|
+
extends: [
|
|
54
|
+
suspensePlugin.configs.recommended, // Apply recommended config
|
|
55
|
+
],
|
|
56
|
+
//... other configs
|
|
57
|
+
]);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### π Rules Description
|
|
61
|
+
|
|
62
|
+
Checks the names of hooks and components that trigger `Suspense`.
|
|
63
|
+
|
|
64
|
+
β Incorrect Examples
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
// 1. Hook naming violation
|
|
68
|
+
// Triggers Suspense but named like a normal hook
|
|
69
|
+
function useUserData() {
|
|
70
|
+
throw promise;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 2. Component naming violation
|
|
74
|
+
// Uses a Suspense hook but named like a normal component
|
|
75
|
+
function UserProfile() {
|
|
76
|
+
const data = useSuspenseUser(); // <--- β οΈ Warning: Change component name to 'SuspenseUserProfile'.
|
|
77
|
+
return <div>{data.name}</div>;
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
β
Correct Examples
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
// 1. Hook name starts with 'useSuspense'
|
|
85
|
+
function useSuspenseUserData() {
|
|
86
|
+
throw promise;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 2. Component name starts with 'Suspense'
|
|
90
|
+
// The caller can tell just by the name that <Suspense> is required.
|
|
91
|
+
function SuspenseUserProfile() {
|
|
92
|
+
const data = useSuspenseUserData();
|
|
93
|
+
return <div>{data.name}</div>;
|
|
94
|
+
}
|
|
95
|
+
```
|
package/index.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const detectSuspenseHook = require('./rules/detect-suspense-hook');
|
|
2
|
+
|
|
3
|
+
const plugin = {
|
|
4
|
+
meta: {
|
|
5
|
+
name: 'eslint-plugin-react-suspense-check',
|
|
6
|
+
version: '1.0.0',
|
|
7
|
+
},
|
|
8
|
+
rules: {
|
|
9
|
+
'detect-suspense-hook': detectSuspenseHook,
|
|
10
|
+
},
|
|
11
|
+
configs: {}, // μλμμ μ±μ
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
plugin.configs.recommended = {
|
|
15
|
+
plugins: {
|
|
16
|
+
'react-suspense-check': plugin,
|
|
17
|
+
},
|
|
18
|
+
rules: {
|
|
19
|
+
'react-suspense-check/detect-suspense-hook': 'warn',
|
|
20
|
+
},
|
|
21
|
+
// β’ (μ νμ¬ν) μ μ©ν νμΌ νμ₯μλ 미리 μ§μ ν΄μ£Όλ©΄ μ¬μ©μκ° νΈν¨
|
|
22
|
+
// μ΄κ±Έ μ λ£μΌλ©΄ λͺ¨λ νμΌ(.css, .json λ±)μ λ€ μ°λ¬λ΄μ λΉν¨μ¨μ μΌ μ μμ
|
|
23
|
+
files: ['**/*.{js,jsx,mjs,cjs,ts,tsx}'],
|
|
24
|
+
languageOptions: {
|
|
25
|
+
parserOptions: {
|
|
26
|
+
ecmaFeatures: {
|
|
27
|
+
jsx: true,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
module.exports = plugin;
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "eslint-plugin-react-suspense-check",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "ESLint plugin to enforce naming conventions for React Suspense safety",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=16.0.0"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"eslint",
|
|
14
|
+
"eslintplugin",
|
|
15
|
+
"eslint-plugin",
|
|
16
|
+
"react",
|
|
17
|
+
"suspense",
|
|
18
|
+
"naming-convention",
|
|
19
|
+
"hook",
|
|
20
|
+
"lint",
|
|
21
|
+
"analysis"
|
|
22
|
+
],
|
|
23
|
+
"author": "Your Name <your.email@example.com> (https://your-website.com)",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "https://github.com/rhehfl/eslint-plugin-react-suspense-check.git"
|
|
28
|
+
},
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/rhehfl/eslint-plugin-react-suspense-check/issues"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/rhehfl/eslint-plugin-react-suspense-check#readme",
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"eslint": ">=8.0.0"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"index.js",
|
|
38
|
+
"rules",
|
|
39
|
+
"package.json",
|
|
40
|
+
"README.md",
|
|
41
|
+
"LICENSE"
|
|
42
|
+
]
|
|
43
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
meta: {
|
|
3
|
+
type: 'problem',
|
|
4
|
+
docs: {
|
|
5
|
+
description:
|
|
6
|
+
'Enforce naming conventions (Chain of Suspense) for hooks triggering Suspense',
|
|
7
|
+
category: 'Best Practices',
|
|
8
|
+
recommended: true,
|
|
9
|
+
},
|
|
10
|
+
messages: {
|
|
11
|
+
// [Case 1] ν
μ΄λ¦ λ³κ²½ νμ
|
|
12
|
+
hookRenamingRequired_en:
|
|
13
|
+
"π This hook triggers Suspense internally. Rename it to start with 'useSuspense' to signal its behavior.\n(Suggested: {{ suggestedName }})",
|
|
14
|
+
hookRenamingRequired_kr:
|
|
15
|
+
"π λ΄λΆμμ Suspenseλ₯Ό μ λ°νλ ν
μ
λλ€. μ΄λ¦μ 'useSuspense'λ‘ μμνκ² λ³κ²½νμ¬ λμμ λͺ
μνμΈμ.\n(μΆμ² μ΄λ¦: {{ suggestedName }})",
|
|
16
|
+
|
|
17
|
+
// [Case 2] μ»΄ν¬λνΈ μ΄λ¦ λ³κ²½ νμ
|
|
18
|
+
componentRenamingRequired_en:
|
|
19
|
+
"π This component uses a Suspense-triggering hook. Rename it to 'Suspense{{name}}' so callers know to wrap it in a <Suspense> boundary.",
|
|
20
|
+
componentRenamingRequired_kr:
|
|
21
|
+
"π λ΄λΆμμ Suspense ν
μ μ¬μ©νλ μ»΄ν¬λνΈμ
λλ€. μμμμ <Suspense> μ²λ¦¬κ° νμν¨μ μ릴 μ μλλ‘ μ΄λ¦μ 'Suspense{{name}}' νμμΌλ‘ λ³κ²½νμΈμ.",
|
|
22
|
+
},
|
|
23
|
+
schema: [
|
|
24
|
+
{
|
|
25
|
+
type: 'object',
|
|
26
|
+
properties: {
|
|
27
|
+
language: {
|
|
28
|
+
type: 'string',
|
|
29
|
+
enum: ['en', 'kr'],
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
additionalProperties: false,
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
create(context) {
|
|
38
|
+
const configuration = context.options[0] || {};
|
|
39
|
+
const lang = configuration.language || 'en';
|
|
40
|
+
return {
|
|
41
|
+
CallExpression(node) {
|
|
42
|
+
let hookName = null;
|
|
43
|
+
if (node.callee.type === 'Identifier') {
|
|
44
|
+
hookName = node.callee.name;
|
|
45
|
+
} else if (node.callee.type === 'MemberExpression') {
|
|
46
|
+
hookName = node.callee.property.name;
|
|
47
|
+
}
|
|
48
|
+
const isSuspenseTrigger =
|
|
49
|
+
hookName === 'use' ||
|
|
50
|
+
hookName === 'lazy' ||
|
|
51
|
+
(typeof hookName === 'string' && /^useSuspense/.test(hookName));
|
|
52
|
+
|
|
53
|
+
if (!isSuspenseTrigger) return;
|
|
54
|
+
|
|
55
|
+
let parent = node.parent;
|
|
56
|
+
while (parent) {
|
|
57
|
+
if (
|
|
58
|
+
parent.type === 'FunctionDeclaration' ||
|
|
59
|
+
parent.type === 'ArrowFunctionExpression' ||
|
|
60
|
+
parent.type === 'FunctionExpression'
|
|
61
|
+
) {
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
parent = parent.parent;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!parent) return;
|
|
68
|
+
|
|
69
|
+
let parentFunctionName = null;
|
|
70
|
+
let parentIdNode = null;
|
|
71
|
+
|
|
72
|
+
if (parent.type === 'FunctionDeclaration' && parent.id) {
|
|
73
|
+
parentFunctionName = parent.id.name;
|
|
74
|
+
parentIdNode = parent.id;
|
|
75
|
+
} else if (
|
|
76
|
+
parent.type === 'ArrowFunctionExpression' ||
|
|
77
|
+
parent.type === 'FunctionExpression'
|
|
78
|
+
) {
|
|
79
|
+
if (parent.parent.type === 'VariableDeclarator' && parent.parent.id) {
|
|
80
|
+
parentFunctionName = parent.parent.id.name;
|
|
81
|
+
parentIdNode = parent.parent.id;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!parentFunctionName) return;
|
|
86
|
+
|
|
87
|
+
const isParentHook = /^use/.test(parentFunctionName);
|
|
88
|
+
|
|
89
|
+
if (isParentHook) {
|
|
90
|
+
if (/^useSuspense/.test(parentFunctionName)) return;
|
|
91
|
+
|
|
92
|
+
const suggestedName = parentFunctionName.replace(
|
|
93
|
+
/^use/,
|
|
94
|
+
'useSuspense'
|
|
95
|
+
);
|
|
96
|
+
context.report({
|
|
97
|
+
node: parentIdNode,
|
|
98
|
+
messageId: `hookRenamingRequired_${lang}`,
|
|
99
|
+
data: {
|
|
100
|
+
suggestedName: suggestedName,
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
} else {
|
|
104
|
+
const isSuspenseComponent = /^Suspense/.test(parentFunctionName);
|
|
105
|
+
|
|
106
|
+
if (!isSuspenseComponent) {
|
|
107
|
+
context.report({
|
|
108
|
+
node: parentIdNode,
|
|
109
|
+
messageId: `componentRenamingRequired_${lang}`,
|
|
110
|
+
data: {
|
|
111
|
+
name: parentFunctionName,
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
},
|
|
119
|
+
};
|