@nadicodeai/ui 12.1.144 → 12.1.145
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.
|
@@ -41,6 +41,12 @@ const AUTH_CALLS = new Set([
|
|
|
41
41
|
*/
|
|
42
42
|
const VALIDATION_CALLS = new Set(["parse", "safeParse"]);
|
|
43
43
|
|
|
44
|
+
const FUNCTION_TYPES = new Set([
|
|
45
|
+
"FunctionDeclaration",
|
|
46
|
+
"FunctionExpression",
|
|
47
|
+
"ArrowFunctionExpression",
|
|
48
|
+
]);
|
|
49
|
+
|
|
44
50
|
function hasUseServer(body) {
|
|
45
51
|
return (body ?? []).some(
|
|
46
52
|
(statement) =>
|
|
@@ -90,12 +96,40 @@ function exportedIdentifier(node) {
|
|
|
90
96
|
return undefined;
|
|
91
97
|
}
|
|
92
98
|
|
|
99
|
+
function inServerActionCode(node, moduleIsServer) {
|
|
100
|
+
if (moduleIsServer) return true;
|
|
101
|
+
for (let ancestor = node.parent; ancestor; ancestor = ancestor.parent) {
|
|
102
|
+
if (FUNCTION_TYPES.has(ancestor.type) && hasUseServer(ancestor.body?.body)) return true;
|
|
103
|
+
}
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function inlineRejectionHandler(node) {
|
|
108
|
+
const handler = node.arguments[0];
|
|
109
|
+
if (node.callee.type !== "MemberExpression" || node.callee.property.type !== "Identifier") {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
if (node.callee.property.name !== "catch" || !FUNCTION_TYPES.has(handler?.type)) return undefined;
|
|
113
|
+
return handler;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function dropsTheError(sourceCode, node, bindings) {
|
|
117
|
+
if (bindings.length === 0) return true;
|
|
118
|
+
return sourceCode
|
|
119
|
+
.getDeclaredVariables(node)
|
|
120
|
+
.filter((variable) =>
|
|
121
|
+
variable.defs.some((def) => def.type === "CatchClause" || def.type === "Parameter"),
|
|
122
|
+
)
|
|
123
|
+
.every((variable) => variable.references.length === 0);
|
|
124
|
+
}
|
|
125
|
+
|
|
93
126
|
/** @type {import("eslint").Rule.RuleModule} */
|
|
94
127
|
export const serverActionDiscipline = {
|
|
95
128
|
meta: {
|
|
96
129
|
type: "problem",
|
|
97
130
|
docs: {
|
|
98
|
-
description:
|
|
131
|
+
description:
|
|
132
|
+
"An exported server action resolves its viewer and parses its client input, and server-action code never drops an error it catches.",
|
|
99
133
|
},
|
|
100
134
|
schema: [],
|
|
101
135
|
messages: {
|
|
@@ -103,10 +137,13 @@ export const serverActionDiscipline = {
|
|
|
103
137
|
"Server action '{{name}}' calls no viewer check. Resolve the viewer first (actionActor, requirePortalViewer, companyActionContext, …), or disable this rule inline with the reason it is public.",
|
|
104
138
|
missingValidation:
|
|
105
139
|
"Server action '{{name}}' takes client input and never parses it. Run the Zod schema's parse/safeParse before trusting the argument.",
|
|
140
|
+
droppedError:
|
|
141
|
+
"This catch drops the error it handles, so an outage reaches the form as an ordinary answer and leaves no log line. Name the error and translate it, answer the form's unavailable state with actionUnavailable, which logs it once, or rethrow it.",
|
|
106
142
|
},
|
|
107
143
|
},
|
|
108
144
|
create(context) {
|
|
109
|
-
const
|
|
145
|
+
const { sourceCode } = context;
|
|
146
|
+
const moduleIsServer = hasUseServer(sourceCode.ast.body);
|
|
110
147
|
/**
|
|
111
148
|
* One frame per function, holding only the calls written in that function's
|
|
112
149
|
* own body. A call parked inside a nested callback is not a call the action
|
|
@@ -160,7 +197,21 @@ export const serverActionDiscipline = {
|
|
|
160
197
|
if (local?.type === "Identifier") specifierExports.add(local.name);
|
|
161
198
|
}
|
|
162
199
|
},
|
|
200
|
+
CatchClause(node) {
|
|
201
|
+
const bindings = node.param ? [node.param] : [];
|
|
202
|
+
if (inServerActionCode(node, moduleIsServer) && dropsTheError(sourceCode, node, bindings)) {
|
|
203
|
+
context.report({ node, messageId: "droppedError" });
|
|
204
|
+
}
|
|
205
|
+
},
|
|
163
206
|
CallExpression(node) {
|
|
207
|
+
const handler = inlineRejectionHandler(node);
|
|
208
|
+
if (
|
|
209
|
+
handler &&
|
|
210
|
+
inServerActionCode(node, moduleIsServer) &&
|
|
211
|
+
dropsTheError(sourceCode, handler, handler.params)
|
|
212
|
+
) {
|
|
213
|
+
context.report({ node: handler, messageId: "droppedError" });
|
|
214
|
+
}
|
|
164
215
|
const name = calleeName(node);
|
|
165
216
|
if (!name) return;
|
|
166
217
|
callStack.at(-1)?.add(name);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nadicodeai/ui",
|
|
3
|
-
"version": "12.1.
|
|
3
|
+
"version": "12.1.145",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"@base-ui/react": "^1.9.0",
|
|
84
84
|
"@iconify-icons/logos": "^2.0.3",
|
|
85
85
|
"@iconify/react": "^6.0.2",
|
|
86
|
-
"@nadicodeai/design-system": "12.1.
|
|
86
|
+
"@nadicodeai/design-system": "12.1.145",
|
|
87
87
|
"@tanstack/react-table": "^9.3.1",
|
|
88
88
|
"@vgpu/wgsl": "0.6.0",
|
|
89
89
|
"class-variance-authority": "^0.7.1",
|