Skip to content

Commit 4d1e087

Browse files
jorgezreikljharb
authored andcommitted
[New] async-server-action: Add rule to require that server actions be async
1 parent a944aa5 commit 4d1e087

File tree

6 files changed

+677
-0
lines changed

6 files changed

+677
-0
lines changed

CHANGELOG.md

+7
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
55

66
## Unreleased
77

8+
### Added
9+
10+
- [`async-server-action`]: add rule ([#3729][] @jorgezreik)
11+
12+
[#3729]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3729
13+
814
### Fixed
915
* [`prop-types`]: null-check rootNode before calling getScope ([#3762][] @crnhrv)
1016
* [`boolean-prop-naming`]: avoid a crash with a spread prop ([#3733][] @ljharb)
@@ -40,6 +46,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
4046
* [`no-unknown-property`]: support `popover`, `popovertarget`, `popovertargetaction` attributes ([#3707][] @ljharb)
4147
* [`no-unknown-property`]: only match `data-*` attributes containing `-` ([#3713][] @silverwind)
4248
* [`checked-requires-onchange-or-readonly`]: correct options that were behaving opposite ([#3715][] @jaesoekjjang)
49+
* [`boolean-prop-naming`]: avoid a crash with a non-TSTypeReference type ([#3718][] @developer-bandi)
4350

4451
### Changed
4552
* [`boolean-prop-naming`]: improve error message (@ljharb)

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,7 @@ module.exports = [
293293

294294
| Name                                  | Description | 💼 | 🚫 | 🔧 | 💡 ||
295295
| :------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------- | :- | :- | :- | :- | :- |
296+
| [async-server-action](docs/rules/async-server-action.md) | Require functions with the `use server` directive to be async | | | | 💡 | |
296297
| [boolean-prop-naming](docs/rules/boolean-prop-naming.md) | Enforces consistent naming for boolean props | | | | | |
297298
| [button-has-type](docs/rules/button-has-type.md) | Disallow usage of `button` elements without an explicit `type` attribute | | | | | |
298299
| [checked-requires-onchange-or-readonly](docs/rules/checked-requires-onchange-or-readonly.md) | Enforce using `onChange` or `readonly` attribute when `checked` is used | | | | | |

docs/rules/async-server-action.md

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Require functions with the `use server` directive to be async (`react/async-server-action`)
2+
3+
💡 This rule is manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).
4+
5+
<!-- end auto-generated rule header -->
6+
7+
Require Server Actions (functions with the `use server` directive) to be async, as mandated by the `use server` [spec](https://react.dev/reference/react/use-server).
8+
9+
This must be the case even if the function does not use `await` or `return` a promise.
10+
11+
## Rule Details
12+
13+
Examples of **incorrect** code for this rule:
14+
15+
```jsx
16+
<form
17+
action={() => {
18+
'use server';
19+
...
20+
}}
21+
>
22+
...
23+
</form>
24+
```
25+
26+
```jsx
27+
function action() {
28+
'use server';
29+
...
30+
}
31+
```
32+
33+
Examples of **correct** code for this rule:
34+
35+
```jsx
36+
<form
37+
action={async () => {
38+
'use server';
39+
...
40+
}}
41+
>
42+
...
43+
</form>
44+
```
45+
46+
```jsx
47+
async function action() {
48+
'use server';
49+
...
50+
}
51+
```
52+
53+
## When Not To Use It
54+
55+
If you are not using React Server Components.

lib/rules/async-server-action.js

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* @fileoverview Require functions with the `use server` directive to be async
3+
* @author Jorge Zreik
4+
*/
5+
6+
'use strict';
7+
8+
const docsUrl = require('../util/docsUrl');
9+
const report = require('../util/report');
10+
11+
// ------------------------------------------------------------------------------
12+
// Rule Definition
13+
// ------------------------------------------------------------------------------
14+
15+
const messages = {
16+
asyncServerAction: 'Server Actions must be async',
17+
suggestAsync: 'Make {{functionName}} async',
18+
};
19+
20+
/** @type {import('eslint').Rule.RuleModule} */
21+
module.exports = {
22+
meta: {
23+
docs: {
24+
description: 'Require functions with the `use server` directive to be async',
25+
category: 'Possible Errors',
26+
recommended: false,
27+
url: docsUrl('async-server-action'),
28+
},
29+
30+
messages,
31+
32+
type: 'suggestion',
33+
hasSuggestions: true,
34+
35+
schema: [],
36+
},
37+
38+
create(context) {
39+
return {
40+
':function[async=false][generator=false]>BlockStatement>:first-child[expression.value="use server"]'(node) {
41+
const currentFunction = node.parent.parent;
42+
const functionName = currentFunction.id ? `\`${currentFunction.id.name}\`` : 'this function';
43+
44+
const data = { functionName };
45+
report(context, messages.asyncServerAction, 'asyncServerAction', {
46+
node: currentFunction,
47+
data,
48+
suggest: [{
49+
desc: messages.suggestAsync,
50+
data,
51+
fix(fixer) {
52+
return fixer.insertTextBefore(currentFunction, 'async ');
53+
},
54+
}],
55+
});
56+
},
57+
};
58+
},
59+
};

lib/rules/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
/** @type {Record<string, import('eslint').Rule.RuleModule>} */
66
module.exports = {
7+
'async-server-action': require('./async-server-action'),
78
'boolean-prop-naming': require('./boolean-prop-naming'),
89
'button-has-type': require('./button-has-type'),
910
'checked-requires-onchange-or-readonly': require('./checked-requires-onchange-or-readonly'),

0 commit comments

Comments
 (0)