Skip to content

Commit 3ecf087

Browse files
[TypeScript Plugin] Moved the diagnostics' positions to the prop's type instead of the value for client-boundary warnings (vercel#79193)
Best be reviewed with "Hide whitespace". ### Why? Since it was matching `ts.isObjectBindingPattern`, which checks only the destructured props: ![CleanShot 2025-05-14 at 12 17 25@2x](https://github.com/user-attachments/assets/44aaffd2-9276-486e-86ce-acb77d488ab0) It didn't handle non-destructured: ![CleanShot 2025-05-14 at 12 17 34@2x](https://github.com/user-attachments/assets/23abc130-3e17-4f72-89a2-bc084841f52f) Therefore, I think it's more natural to target the types, not the props value, as the cause of this warning is actually the types: ![CleanShot 2025-05-14 at 12 18 00@2x](https://github.com/user-attachments/assets/0a5a64c8-1ac9-4b0f-8fe7-20e78db2acec) ![CleanShot 2025-05-14 at 12 17 53@2x](https://github.com/user-attachments/assets/72008c25-7d7b-46e4-8e2f-e52f01e50b80) x-ref: vercel#79144 (comment) --------- Co-authored-by: Janka Uryga <lolzatu2@gmail.com>
1 parent 8c56664 commit 3ecf087

File tree

6 files changed

+82
-84
lines changed

6 files changed

+82
-84
lines changed

.changeset/shy-impalas-add.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"next": patch
3+
---
4+
5+
[TypeScript Plugin] Moved the diagnostics' positions to the prop's type instead of the value for client-boundary warnings.

packages/next/src/server/typescript/rules/client-boundary.ts

Lines changed: 62 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -43,57 +43,69 @@ const clientBoundary = {
4343
const isErrorFile = /[\\/]error\.tsx?$/.test(source.fileName)
4444
const isGlobalErrorFile = /[\\/]global-error\.tsx?$/.test(source.fileName)
4545

46-
const props = node.parameters?.[0]?.name
47-
if (props && ts.isObjectBindingPattern(props)) {
48-
for (const prop of (props as tsModule.ObjectBindingPattern).elements) {
49-
const type = typeChecker.getTypeAtLocation(prop)
50-
const typeDeclarationNode = type.symbol?.getDeclarations()?.[0]
51-
const propName = (prop.propertyName || prop.name).getText()
52-
53-
if (typeDeclarationNode) {
54-
if (ts.isFunctionTypeNode(typeDeclarationNode)) {
55-
// By convention, props named "action" can accept functions since we
56-
// assume these are Server Actions. Structurally, there's no
57-
// difference between a Server Action and a normal function until
58-
// TypeScript exposes directives in the type of a function. This
59-
// will miss accidentally passing normal functions but a false
60-
// negative is better than a false positive given how frequent the
61-
// false-positive would be.
62-
const maybeServerAction =
63-
propName === 'action' || /.+Action$/.test(propName)
64-
65-
// There's a special case for the error file that the `reset` prop
66-
// is allowed to be a function:
67-
// https://github.com/vercel/next.js/issues/46573
68-
const isErrorReset =
69-
(isErrorFile || isGlobalErrorFile) && propName === 'reset'
70-
71-
if (!maybeServerAction && !isErrorReset) {
72-
diagnostics.push({
73-
file: source,
74-
category: ts.DiagnosticCategory.Warning,
75-
code: NEXT_TS_ERRORS.INVALID_CLIENT_ENTRY_PROP,
76-
messageText:
77-
`Props must be serializable for components in the "use client" entry file. ` +
78-
`"${propName}" is a function that's not a Server Action. ` +
79-
`Rename "${propName}" either to "action" or have its name end with "Action" e.g. "${propName}Action" to indicate it is a Server Action.`,
80-
start: prop.getStart(),
81-
length: prop.getWidth(),
82-
})
46+
const props = node.parameters?.[0]
47+
if (props) {
48+
const propsType = typeChecker.getTypeAtLocation(props)
49+
const typeNode = propsType.symbol?.getDeclarations()?.[0]
50+
51+
if (typeNode && ts.isTypeLiteralNode(typeNode)) {
52+
for (const member of typeNode.members) {
53+
if (ts.isPropertySignature(member)) {
54+
const propName = member.name.getText()
55+
const propType = member.type
56+
57+
if (propType) {
58+
const propTypeInfo = typeChecker.getTypeAtLocation(propType)
59+
const typeDeclarationNode =
60+
propTypeInfo.symbol?.getDeclarations()?.[0]
61+
62+
if (typeDeclarationNode) {
63+
if (ts.isFunctionTypeNode(typeDeclarationNode)) {
64+
// By convention, props named "action" can accept functions since we
65+
// assume these are Server Actions. Structurally, there's no
66+
// difference between a Server Action and a normal function until
67+
// TypeScript exposes directives in the type of a function. This
68+
// will miss accidentally passing normal functions but a false
69+
// negative is better than a false positive given how frequent the
70+
// false-positive would be.
71+
const maybeServerAction =
72+
propName === 'action' || /.+Action$/.test(propName)
73+
74+
// There's a special case for the error file that the `reset` prop
75+
// is allowed to be a function:
76+
// https://github.com/vercel/next.js/issues/46573
77+
const isErrorReset =
78+
(isErrorFile || isGlobalErrorFile) && propName === 'reset'
79+
80+
if (!maybeServerAction && !isErrorReset) {
81+
diagnostics.push({
82+
file: source,
83+
category: ts.DiagnosticCategory.Warning,
84+
code: NEXT_TS_ERRORS.INVALID_CLIENT_ENTRY_PROP,
85+
messageText:
86+
`Props must be serializable for components in the "use client" entry file. ` +
87+
`"${propName}" is a function that's not a Server Action. ` +
88+
`Rename "${propName}" either to "action" or have its name end with "Action" e.g. "${propName}Action" to indicate it is a Server Action.`,
89+
start: propType.getStart(),
90+
length: propType.getWidth(),
91+
})
92+
}
93+
} else if (
94+
// Show warning for not serializable props.
95+
ts.isConstructorTypeNode(typeDeclarationNode) ||
96+
ts.isClassDeclaration(typeDeclarationNode)
97+
) {
98+
diagnostics.push({
99+
file: source,
100+
category: ts.DiagnosticCategory.Warning,
101+
code: NEXT_TS_ERRORS.INVALID_CLIENT_ENTRY_PROP,
102+
messageText: `Props must be serializable for components in the "use client" entry file, "${propName}" is invalid.`,
103+
start: propType.getStart(),
104+
length: propType.getWidth(),
105+
})
106+
}
107+
}
83108
}
84-
} else if (
85-
// Show warning for not serializable props.
86-
ts.isConstructorTypeNode(typeDeclarationNode) ||
87-
ts.isClassDeclaration(typeDeclarationNode)
88-
) {
89-
diagnostics.push({
90-
file: source,
91-
category: ts.DiagnosticCategory.Warning,
92-
code: NEXT_TS_ERRORS.INVALID_CLIENT_ENTRY_PROP,
93-
messageText: `Props must be serializable for components in the "use client" entry file, "${propName}" is invalid.`,
94-
start: prop.getStart(),
95-
length: prop.getWidth(),
96-
})
97109
}
98110
}
99111
}

test/development/typescript-plugin/client-boundary/app/non-serializable-action-props.tsx

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,7 @@ class Class {}
44

55
type ArrowFunctionTypeAlias = () => void
66

7-
export default function ClientComponent({
8-
_arrowFunctionAction,
9-
_arrowFunctionTypeAliasAction,
10-
// Doesn't make sense, but check for loophole
11-
_classAction,
12-
_constructorAction,
13-
}: {
7+
export default function ClientComponent(props: {
148
_arrowFunctionAction: () => void
159
_arrowFunctionTypeAliasAction: ArrowFunctionTypeAlias
1610
// Doesn't make sense, but check for loophole

test/development/typescript-plugin/client-boundary/app/non-serializable-props.tsx

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,7 @@ class Class {}
44

55
type ArrowFunctionTypeAlias = () => void
66

7-
export default function ClientComponent({
8-
_arrowFunction,
9-
_arrowFunctionTypeAlias,
10-
_class,
11-
_constructor,
12-
}: {
7+
export default function ClientComponent(props: {
138
_arrowFunction: () => void
149
_arrowFunctionTypeAlias: ArrowFunctionTypeAlias
1510
_class: Class

test/development/typescript-plugin/client-boundary/app/serializable-props.tsx

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,6 @@
11
'use client'
22

3-
export default function ClientComponent({
4-
_string,
5-
_number,
6-
_boolean,
7-
_array,
8-
_object,
9-
_null,
10-
_undefined,
11-
}: {
3+
export default function ClientComponent(props: {
124
_string: string
135
_number: number
146
_boolean: boolean

test/development/typescript-plugin/client-boundary/client-boundary.test.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -58,15 +58,15 @@ describe('typescript-plugin - client-boundary', () => {
5858
"app/non-serializable-action-props.tsx": [
5959
{
6060
"code": 71007,
61-
"length": 12,
61+
"length": 5,
6262
"messageText": "Props must be serializable for components in the "use client" entry file, "_classAction" is invalid.",
63-
"start": 221,
63+
"start": 276,
6464
},
6565
{
6666
"code": 71007,
67-
"length": 18,
67+
"length": 14,
6868
"messageText": "Props must be serializable for components in the "use client" entry file, "_constructorAction" is invalid.",
69-
"start": 237,
69+
"start": 304,
7070
},
7171
],
7272
}
@@ -91,27 +91,27 @@ describe('typescript-plugin - client-boundary', () => {
9191
"app/non-serializable-props.tsx": [
9292
{
9393
"code": 71007,
94-
"length": 14,
94+
"length": 10,
9595
"messageText": "Props must be serializable for components in the "use client" entry file. "_arrowFunction" is a function that's not a Server Action. Rename "_arrowFunction" either to "action" or have its name end with "Action" e.g. "_arrowFunctionAction" to indicate it is a Server Action.",
96-
"start": 116,
96+
"start": 139,
9797
},
9898
{
9999
"code": 71007,
100-
"length": 23,
100+
"length": 22,
101101
"messageText": "Props must be serializable for components in the "use client" entry file. "_arrowFunctionTypeAlias" is a function that's not a Server Action. Rename "_arrowFunctionTypeAlias" either to "action" or have its name end with "Action" e.g. "_arrowFunctionTypeAliasAction" to indicate it is a Server Action.",
102-
"start": 134,
102+
"start": 177,
103103
},
104104
{
105105
"code": 71007,
106-
"length": 6,
106+
"length": 5,
107107
"messageText": "Props must be serializable for components in the "use client" entry file, "_class" is invalid.",
108-
"start": 161,
108+
"start": 210,
109109
},
110110
{
111111
"code": 71007,
112-
"length": 12,
112+
"length": 14,
113113
"messageText": "Props must be serializable for components in the "use client" entry file, "_constructor" is invalid.",
114-
"start": 171,
114+
"start": 232,
115115
},
116116
],
117117
}

0 commit comments

Comments
 (0)