Skip to content

Commit 561edf4

Browse files
committed
[New] async-server-action: Add rule to require that server actions be async
1 parent e4ecbcf commit 561edf4

File tree

6 files changed

+2363
-1439
lines changed

6 files changed

+2363
-1439
lines changed

CHANGELOG.md

Lines changed: 1948 additions & 1439 deletions
Large diffs are not rendered by default.

configs/recommended.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const all = require('./all');
55
module.exports = Object.assign({}, all, {
66
languageOptions: all.languageOptions,
77
rules: {
8+
'react/async-server-action': 2,
89
'react/display-name': 2,
910
'react/jsx-key': 2,
1011
'react/jsx-no-comment-textnodes': 2,

docs/rules/async-server-action.md

Lines changed: 55 additions & 0 deletions
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 enabled in the ☑️ `recommended` [config](https://github.com/jsx-eslint/eslint-plugin-react/#shareable-configs).
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

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
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: 'Your server action should be async',
17+
};
18+
19+
/**
20+
* Detects a `use server` directive in a given AST node
21+
* @param {ASTNode} node The node to search.
22+
* @returns {boolean} Whether the node given has a `use server` directive.
23+
*/
24+
function hasUseServerDirective(node) {
25+
if (node.body.type !== 'BlockStatement') return false;
26+
27+
const functionBody = node.body.body;
28+
if (functionBody.length === 0) return false;
29+
30+
const potentialDirectiveStatement = functionBody[0];
31+
if (potentialDirectiveStatement.type !== 'ExpressionStatement') return false;
32+
33+
const potentialDirectiveExpression = potentialDirectiveStatement.expression;
34+
if (potentialDirectiveExpression.type !== 'Literal') return false;
35+
36+
return potentialDirectiveExpression.value === 'use server';
37+
}
38+
39+
module.exports = {
40+
meta: {
41+
docs: {
42+
description:
43+
'Require functions with the `use server` directive to be async',
44+
category: 'Possible Errors',
45+
recommended: true,
46+
url: docsUrl('async-server-action'),
47+
},
48+
49+
messages,
50+
51+
fixable: 'code',
52+
53+
schema: [],
54+
},
55+
56+
create(context) {
57+
/**
58+
* Validates that given AST node is async if it has the `use server` directive
59+
* @param {ASTNode} node The node to search.
60+
* @returns {void}
61+
*/
62+
function validate(node) {
63+
if (hasUseServerDirective(node) && !node.async) {
64+
report(context, messages.asyncServerAction, 'asyncServerAction', {
65+
node,
66+
fix(fixer) {
67+
return fixer.insertTextBefore(node, 'async ');
68+
},
69+
});
70+
}
71+
}
72+
73+
return {
74+
FunctionDeclaration(node) {
75+
validate(node);
76+
},
77+
FunctionExpression(node) {
78+
validate(node);
79+
},
80+
ArrowFunctionExpression(node) {
81+
validate(node);
82+
},
83+
};
84+
},
85+
};

lib/rules/index.js

Lines changed: 1 addition & 0 deletions
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)