eslint-plugin-react-rsc 5.10.1 → 5.10.3

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 (2) hide show
  1. package/dist/index.js +108 -111
  2. package/package.json +8 -8
package/dist/index.js CHANGED
@@ -34,7 +34,7 @@ const rules$4 = { "react-rsc/function-definition": "off" };
34
34
  //#endregion
35
35
  //#region package.json
36
36
  var name$4 = "eslint-plugin-react-rsc";
37
- var version = "5.10.1";
37
+ var version = "5.10.3";
38
38
 
39
39
  //#endregion
40
40
  //#region src/utils/create-rule.ts
@@ -66,148 +66,145 @@ var function_definition_default = createRule({
66
66
  create,
67
67
  defaultOptions: []
68
68
  });
69
+ function isDirectiveName(value) {
70
+ return value === "use client" || value === "use server";
71
+ }
72
+ /**
73
+ * Match a statement against directive-like expressions (`'use client'`, `"use server"`, `` `use server` ``, etc.)
74
+ * @param stmt The statement to match
75
+ * @returns The match result, or `null` if the statement is not directive-like
76
+ */
77
+ function matchDirective(stmt) {
78
+ if (stmt.type !== AST_NODE_TYPES.ExpressionStatement) return null;
79
+ const { expression } = stmt;
80
+ if (Check.isStringLiteral(expression)) {
81
+ if (!isDirectiveName(expression.value)) return null;
82
+ return {
83
+ kind: stmt.directive != null ? "well-formed" : "misplaced",
84
+ name: expression.value,
85
+ node: stmt
86
+ };
87
+ }
88
+ if (expression.type === AST_NODE_TYPES.TemplateLiteral && expression.expressions.length === 0 && expression.quasis.length === 1) {
89
+ const value = expression.quasis[0]?.value.cooked;
90
+ if (!isDirectiveName(value)) return null;
91
+ return {
92
+ kind: "backtick",
93
+ name: value,
94
+ node: stmt
95
+ };
96
+ }
97
+ return null;
98
+ }
69
99
  function create(context) {
70
- const hasUseServer = context.sourceCode.text.includes("use server");
71
- const hasUseClient = context.sourceCode.text.includes("use client");
72
- if (!hasUseServer && !hasUseClient) return {};
100
+ const text = context.sourceCode.text;
101
+ if (!text.includes("use server") && !text.includes("use client")) return {};
73
102
  const hasFileLevelUseServerDirective = context.sourceCode.ast.body.some(Check.isDirective("use server"));
74
103
  /**
75
- * Check if `node` is an async function, and report if not
76
- * @param node The function node to check
77
- * @returns Whether a report was made
104
+ * Build a fix that makes `node` an async function
105
+ * @param node The function node to fix
106
+ * @returns The fix function, or `null` if no fix is available
78
107
  */
79
108
  function getAsyncFix(node) {
80
- if (node.type === AST_NODE_TYPES.FunctionDeclaration || node.type === AST_NODE_TYPES.FunctionExpression) {
81
- const fnToken = context.sourceCode.getFirstToken(node);
82
- if (fnToken != null) return (fixer) => fixer.insertTextBefore(fnToken, "async ");
83
- return null;
84
- }
85
109
  if (node.type === AST_NODE_TYPES.ArrowFunctionExpression) return (fixer) => fixer.insertTextBefore(node, "async ");
86
- return null;
87
- }
88
- function reportNonAsyncFunction(node, messageId) {
89
- if (node == null) return false;
90
- const unwrapped = Extract.unwrap(node);
91
- if (!Check.isFunction(unwrapped)) return false;
92
- if (!unwrapped.async) {
93
- context.report({
94
- fix: getAsyncFix(unwrapped),
95
- messageId,
96
- node: unwrapped
97
- });
98
- return true;
99
- }
100
- return false;
101
- }
102
- /**
103
- * Check non-exported local functions for 'use server' directives, and report if they are not async
104
- * @param node The function node to check
105
- */
106
- function checkLocalServerFunction(node) {
107
- if (core.getFunctionDirectives(node).some((d) => d.directive === "use server")) reportNonAsyncFunction(node, "local");
110
+ const functionToken = context.sourceCode.getFirstToken(node);
111
+ if (functionToken == null) return null;
112
+ return (fixer) => fixer.insertTextBefore(functionToken, "async ");
108
113
  }
109
114
  /**
110
- * Find function declarations from exports and check them
111
- * @param id The identifier of the exported function
115
+ * Report `node` if it resolves to a non-async function
116
+ * @param node The node to check, may be `null` for convenience at call sites
117
+ * @param messageId The message to report with
112
118
  */
113
- function findAndCheckExportedFunctionDeclarations(id) {
114
- const initNode = resolve(context, id);
115
- if (initNode == null) return;
116
- const unwrapped = Extract.unwrap(initNode);
117
- if (!Check.isFunction(unwrapped)) return;
118
- reportNonAsyncFunction(unwrapped, "file");
119
+ function reportNonAsyncFunction(node, messageId) {
120
+ if (node == null) return;
121
+ const fn = Extract.unwrap(node);
122
+ if (!Check.isFunction(fn) || fn.async) return;
123
+ context.report({
124
+ fix: getAsyncFix(fn),
125
+ messageId,
126
+ node: fn
127
+ });
119
128
  }
120
129
  /**
121
130
  * Check file-level directives for correct position and quote style.
122
- * Well-formed directives at the beginning of the file will have a `directive` property.
123
- * If they appear after other code, the parser will not set `directive`.
131
+ * @param program The Program node
124
132
  */
125
- function checkFileLevelDirectives() {
126
- for (const node of context.sourceCode.ast.body) {
127
- if (node.type !== AST_NODE_TYPES.ExpressionStatement) continue;
128
- if (Check.isStringLiteral(node.expression)) {
129
- const value = node.expression.value;
130
- if ((value === "use server" || value === "use client") && node.directive == null) context.report({
131
- data: { name: value },
132
- messageId: "fileDirectivePosition",
133
- node
134
- });
135
- continue;
136
- }
137
- if (node.expression.type === AST_NODE_TYPES.TemplateLiteral && node.expression.quasis.length === 1 && node.expression.expressions.length === 0) {
138
- const value = node.expression.quasis[0]?.value.cooked;
139
- if (value === "use server" || value === "use client") context.report({
140
- data: { name: value },
141
- messageId: "fileDirectiveQuote",
142
- node
143
- });
144
- }
133
+ function checkFileDirectives(program) {
134
+ for (const stmt of program.body) {
135
+ const match = matchDirective(stmt);
136
+ if (match == null || match.kind === "well-formed") continue;
137
+ context.report({
138
+ data: { name: match.name },
139
+ messageId: match.kind === "backtick" ? "fileDirectiveQuote" : "fileDirectivePosition",
140
+ node: match.node
141
+ });
145
142
  }
146
143
  }
147
144
  /**
148
- * Check function-level directives for correct position and quote style.
145
+ * Check function-level directives for correct position and quote style,
146
+ * and report if a `use server` function is not async.
149
147
  * @param node The function node to check
150
148
  */
151
- function checkFunctionDirectives(node) {
149
+ function checkFunction(node) {
152
150
  if (node.body.type !== AST_NODE_TYPES.BlockStatement) return;
153
151
  for (const stmt of node.body.body) {
154
- if (stmt.type !== AST_NODE_TYPES.ExpressionStatement) continue;
155
- if (Check.isStringLiteral(stmt.expression)) {
156
- const value = stmt.expression.value;
157
- if (value === "use server" && stmt.directive == null) context.report({
158
- data: { name: value },
159
- messageId: "localDirectivePosition",
160
- node: stmt
161
- });
162
- if (value === "use client") context.report({
163
- data: { name: value },
164
- messageId: "localDirectiveUnexpected",
165
- node: stmt
166
- });
167
- continue;
168
- }
169
- if (stmt.expression.type === AST_NODE_TYPES.TemplateLiteral && stmt.expression.quasis.length === 1 && stmt.expression.expressions.length === 0) {
170
- const value = stmt.expression.quasis[0]?.value.cooked;
171
- if (value === "use server" || value === "use client") context.report({
172
- data: { name: value },
173
- messageId: "localDirectiveQuote",
174
- node: stmt
175
- });
152
+ const match = matchDirective(stmt);
153
+ if (match == null) continue;
154
+ switch (true) {
155
+ case match.kind === "backtick":
156
+ context.report({
157
+ data: { name: match.name },
158
+ messageId: "localDirectiveQuote",
159
+ node: match.node
160
+ });
161
+ break;
162
+ case match.name === "use client":
163
+ context.report({
164
+ data: { name: match.name },
165
+ messageId: "localDirectiveUnexpected",
166
+ node: match.node
167
+ });
168
+ break;
169
+ case match.kind === "misplaced":
170
+ context.report({
171
+ data: { name: match.name },
172
+ messageId: "localDirectivePosition",
173
+ node: match.node
174
+ });
175
+ break;
176
176
  }
177
177
  }
178
+ if (core.isFunctionHasDirective(node, "use server")) reportNonAsyncFunction(node, "local");
178
179
  }
179
180
  return {
180
- ArrowFunctionExpression(node) {
181
- checkFunctionDirectives(node);
182
- checkLocalServerFunction(node);
183
- },
181
+ ArrowFunctionExpression: checkFunction,
184
182
  ExportDefaultDeclaration(node) {
185
183
  if (!hasFileLevelUseServerDirective) return;
186
- const decl = node.declaration;
187
- if (reportNonAsyncFunction(decl, "file")) return;
188
- if (decl.type === AST_NODE_TYPES.Identifier) findAndCheckExportedFunctionDeclarations(decl);
184
+ const decl = Extract.unwrap(node.declaration);
185
+ if (decl.type === AST_NODE_TYPES.Identifier) {
186
+ reportNonAsyncFunction(resolve(context, decl), "file");
187
+ return;
188
+ }
189
+ reportNonAsyncFunction(decl, "file");
189
190
  },
190
191
  ExportNamedDeclaration(node) {
191
192
  if (!hasFileLevelUseServerDirective) return;
192
- if (node.declaration != null) {
193
- const decl = node.declaration;
194
- if (reportNonAsyncFunction(decl, "file")) return;
195
- if (decl.type === AST_NODE_TYPES.VariableDeclaration) for (const declarator of decl.declarations) reportNonAsyncFunction(declarator.init, "file");
193
+ const decl = node.declaration;
194
+ if (decl != null) {
195
+ if (decl.type === AST_NODE_TYPES.VariableDeclaration) {
196
+ for (const declarator of decl.declarations) reportNonAsyncFunction(declarator.init, "file");
197
+ return;
198
+ }
199
+ reportNonAsyncFunction(decl, "file");
196
200
  return;
197
201
  }
198
- if (node.source == null && node.specifiers.length > 0) for (const spec of node.specifiers) findAndCheckExportedFunctionDeclarations(spec.local);
202
+ if (node.source != null) return;
203
+ for (const spec of node.specifiers) reportNonAsyncFunction(resolve(context, spec.local), "file");
199
204
  },
200
- FunctionDeclaration(node) {
201
- checkFunctionDirectives(node);
202
- checkLocalServerFunction(node);
203
- },
204
- FunctionExpression(node) {
205
- checkFunctionDirectives(node);
206
- checkLocalServerFunction(node);
207
- },
208
- Program() {
209
- checkFileLevelDirectives();
210
- }
205
+ FunctionDeclaration: checkFunction,
206
+ FunctionExpression: checkFunction,
207
+ Program: checkFileDirectives
211
208
  };
212
209
  }
213
210
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-react-rsc",
3
- "version": "5.10.1",
3
+ "version": "5.10.3",
4
4
  "description": "ESLint React's ESLint plugin for RSC related rules.",
5
5
  "keywords": [
6
6
  "react",
@@ -38,11 +38,11 @@
38
38
  "dependencies": {
39
39
  "@typescript-eslint/types": "^8.62.1",
40
40
  "@typescript-eslint/utils": "^8.62.1",
41
- "@eslint-react/ast": "5.10.1",
42
- "@eslint-react/shared": "5.10.1",
43
- "@eslint-react/core": "5.10.1",
44
- "@eslint-react/var": "5.10.1",
45
- "@eslint-react/eslint": "5.10.1"
41
+ "@eslint-react/ast": "5.10.3",
42
+ "@eslint-react/core": "5.10.3",
43
+ "@eslint-react/shared": "5.10.3",
44
+ "@eslint-react/eslint": "5.10.3",
45
+ "@eslint-react/var": "5.10.3"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/react": "^19.2.17",
@@ -51,8 +51,8 @@
51
51
  "eslint": "^10.6.0",
52
52
  "tsdown": "^0.22.3",
53
53
  "typescript": "6.0.3",
54
- "@local/configs": "0.0.0",
55
- "@local/eff": "0.0.0"
54
+ "@local/eff": "0.0.0",
55
+ "@local/configs": "0.0.0"
56
56
  },
57
57
  "peerDependencies": {
58
58
  "eslint": "*",