From e2d0febc57662f087f9a60bf7542147f413bb97e Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Wed, 14 May 2025 11:37:00 +0200 Subject: [PATCH 1/4] [ts-next-plugin] target props types instead of values for client-boundary warnings --- .../typescript/rules/client-boundary.ts | 112 ++++++++++-------- 1 file changed, 62 insertions(+), 50 deletions(-) diff --git a/packages/next/src/server/typescript/rules/client-boundary.ts b/packages/next/src/server/typescript/rules/client-boundary.ts index 5a6c2a3773e5..c97b8cd0a045 100644 --- a/packages/next/src/server/typescript/rules/client-boundary.ts +++ b/packages/next/src/server/typescript/rules/client-boundary.ts @@ -43,57 +43,69 @@ const clientBoundary = { const isErrorFile = /[\\/]error\.tsx?$/.test(source.fileName) const isGlobalErrorFile = /[\\/]global-error\.tsx?$/.test(source.fileName) - const props = node.parameters?.[0]?.name - if (props && ts.isObjectBindingPattern(props)) { - for (const prop of (props as tsModule.ObjectBindingPattern).elements) { - const type = typeChecker.getTypeAtLocation(prop) - const typeDeclarationNode = type.symbol?.getDeclarations()?.[0] - const propName = (prop.propertyName || prop.name).getText() - - if (typeDeclarationNode) { - if (ts.isFunctionTypeNode(typeDeclarationNode)) { - // By convention, props named "action" can accept functions since we - // assume these are Server Actions. Structurally, there's no - // difference between a Server Action and a normal function until - // TypeScript exposes directives in the type of a function. This - // will miss accidentally passing normal functions but a false - // negative is better than a false positive given how frequent the - // false-positive would be. - const maybeServerAction = - propName === 'action' || /.+Action$/.test(propName) - - // There's a special case for the error file that the `reset` prop - // is allowed to be a function: - // https://github.com/vercel/next.js/issues/46573 - const isErrorReset = - (isErrorFile || isGlobalErrorFile) && propName === 'reset' - - if (!maybeServerAction && !isErrorReset) { - diagnostics.push({ - file: source, - category: ts.DiagnosticCategory.Warning, - code: NEXT_TS_ERRORS.INVALID_CLIENT_ENTRY_PROP, - messageText: - `Props must be serializable for components in the "use client" entry file. ` + - `"${propName}" is a function that's not a Server Action. ` + - `Rename "${propName}" either to "action" or have its name end with "Action" e.g. "${propName}Action" to indicate it is a Server Action.`, - start: prop.getStart(), - length: prop.getWidth(), - }) + const props = node.parameters?.[0] + if (props) { + const propsType = typeChecker.getTypeAtLocation(props) + const typeNode = propsType.symbol?.getDeclarations()?.[0] + + if (typeNode && ts.isTypeLiteralNode(typeNode)) { + for (const member of typeNode.members) { + if (ts.isPropertySignature(member)) { + const propName = member.name.getText() + const propType = member.type + + if (propType) { + const propTypeInfo = typeChecker.getTypeAtLocation(propType) + const typeDeclarationNode = + propTypeInfo.symbol?.getDeclarations()?.[0] + + if (typeDeclarationNode) { + if (ts.isFunctionTypeNode(typeDeclarationNode)) { + // By convention, props named "action" can accept functions since we + // assume these are Server Actions. Structurally, there's no + // difference between a Server Action and a normal function until + // TypeScript exposes directives in the type of a function. This + // will miss accidentally passing normal functions but a false + // negative is better than a false positive given how frequent the + // false-positive would be. + const maybeServerAction = + propName === 'action' || /.+Action$/.test(propName) + + // There's a special case for the error file that the `reset` prop + // is allowed to be a function: + // https://github.com/vercel/next.js/issues/46573 + const isErrorReset = + (isErrorFile || isGlobalErrorFile) && propName === 'reset' + + if (!maybeServerAction && !isErrorReset) { + diagnostics.push({ + file: source, + category: ts.DiagnosticCategory.Warning, + code: NEXT_TS_ERRORS.INVALID_CLIENT_ENTRY_PROP, + messageText: + `Props must be serializable for components in the "use client" entry file. ` + + `"${propName}" is a function that's not a Server Action. ` + + `Rename "${propName}" either to "action" or have its name end with "Action" e.g. "${propName}Action" to indicate it is a Server Action.`, + start: propType.getStart(), + length: propType.getWidth(), + }) + } + } else if ( + // Show warning for not serializable props. + ts.isConstructorTypeNode(typeDeclarationNode) || + ts.isClassDeclaration(typeDeclarationNode) + ) { + diagnostics.push({ + file: source, + category: ts.DiagnosticCategory.Warning, + code: NEXT_TS_ERRORS.INVALID_CLIENT_ENTRY_PROP, + messageText: `Props must be serializable for components in the "use client" entry file, "${propName}" is invalid.`, + start: propType.getStart(), + length: propType.getWidth(), + }) + } + } } - } else if ( - // Show warning for not serializable props. - ts.isConstructorTypeNode(typeDeclarationNode) || - ts.isClassDeclaration(typeDeclarationNode) - ) { - diagnostics.push({ - file: source, - category: ts.DiagnosticCategory.Warning, - code: NEXT_TS_ERRORS.INVALID_CLIENT_ENTRY_PROP, - messageText: `Props must be serializable for components in the "use client" entry file, "${propName}" is invalid.`, - start: prop.getStart(), - length: prop.getWidth(), - }) } } } From d5da7424501fd0f799cb9df4a796bbefb10ada59 Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Wed, 14 May 2025 11:12:51 +0200 Subject: [PATCH 2/4] no destructure Co-authored-by: Janka Uryga --- .../app/non-serializable-action-props.tsx | 8 +------- .../client-boundary/app/non-serializable-props.tsx | 7 +------ .../client-boundary/app/serializable-props.tsx | 10 +--------- 3 files changed, 3 insertions(+), 22 deletions(-) diff --git a/test/development/typescript-plugin/client-boundary/app/non-serializable-action-props.tsx b/test/development/typescript-plugin/client-boundary/app/non-serializable-action-props.tsx index a36c35e9b624..54e47ea1fe64 100644 --- a/test/development/typescript-plugin/client-boundary/app/non-serializable-action-props.tsx +++ b/test/development/typescript-plugin/client-boundary/app/non-serializable-action-props.tsx @@ -4,13 +4,7 @@ class Class {} type ArrowFunctionTypeAlias = () => void -export default function ClientComponent({ - _arrowFunctionAction, - _arrowFunctionTypeAliasAction, - // Doesn't make sense, but check for loophole - _classAction, - _constructorAction, -}: { +export default function ClientComponent(props: { _arrowFunctionAction: () => void _arrowFunctionTypeAliasAction: ArrowFunctionTypeAlias // Doesn't make sense, but check for loophole diff --git a/test/development/typescript-plugin/client-boundary/app/non-serializable-props.tsx b/test/development/typescript-plugin/client-boundary/app/non-serializable-props.tsx index 7c79ef538cc1..45135b2da6f2 100644 --- a/test/development/typescript-plugin/client-boundary/app/non-serializable-props.tsx +++ b/test/development/typescript-plugin/client-boundary/app/non-serializable-props.tsx @@ -4,12 +4,7 @@ class Class {} type ArrowFunctionTypeAlias = () => void -export default function ClientComponent({ - _arrowFunction, - _arrowFunctionTypeAlias, - _class, - _constructor, -}: { +export default function ClientComponent(props: { _arrowFunction: () => void _arrowFunctionTypeAlias: ArrowFunctionTypeAlias _class: Class diff --git a/test/development/typescript-plugin/client-boundary/app/serializable-props.tsx b/test/development/typescript-plugin/client-boundary/app/serializable-props.tsx index 8e1a9d3912ed..26bc7ecba6fb 100644 --- a/test/development/typescript-plugin/client-boundary/app/serializable-props.tsx +++ b/test/development/typescript-plugin/client-boundary/app/serializable-props.tsx @@ -1,14 +1,6 @@ 'use client' -export default function ClientComponent({ - _string, - _number, - _boolean, - _array, - _object, - _null, - _undefined, -}: { +export default function ClientComponent(props: { _string: string _number: number _boolean: boolean From 4bcfb1c36b79c822bf043fe592cc3a626076fe89 Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Wed, 14 May 2025 12:13:02 +0200 Subject: [PATCH 3/4] update snapshot --- .../client-boundary/client-boundary.test.ts | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/test/development/typescript-plugin/client-boundary/client-boundary.test.ts b/test/development/typescript-plugin/client-boundary/client-boundary.test.ts index b678c5040803..02ca4c99a1e0 100644 --- a/test/development/typescript-plugin/client-boundary/client-boundary.test.ts +++ b/test/development/typescript-plugin/client-boundary/client-boundary.test.ts @@ -58,15 +58,15 @@ describe('typescript-plugin - client-boundary', () => { "app/non-serializable-action-props.tsx": [ { "code": 71007, - "length": 12, + "length": 5, "messageText": "Props must be serializable for components in the "use client" entry file, "_classAction" is invalid.", - "start": 221, + "start": 276, }, { "code": 71007, - "length": 18, + "length": 14, "messageText": "Props must be serializable for components in the "use client" entry file, "_constructorAction" is invalid.", - "start": 237, + "start": 304, }, ], } @@ -91,27 +91,27 @@ describe('typescript-plugin - client-boundary', () => { "app/non-serializable-props.tsx": [ { "code": 71007, - "length": 14, + "length": 10, "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.", - "start": 116, + "start": 139, }, { "code": 71007, - "length": 23, + "length": 22, "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.", - "start": 134, + "start": 177, }, { "code": 71007, - "length": 6, + "length": 5, "messageText": "Props must be serializable for components in the "use client" entry file, "_class" is invalid.", - "start": 161, + "start": 210, }, { "code": 71007, - "length": 12, + "length": 14, "messageText": "Props must be serializable for components in the "use client" entry file, "_constructor" is invalid.", - "start": 171, + "start": 232, }, ], } From d60e314b6ccc3fd808ebccf78cfb8edff9b188d3 Mon Sep 17 00:00:00 2001 From: Jiwon Choi Date: Wed, 14 May 2025 14:03:31 +0200 Subject: [PATCH 4/4] Add Changeset --- .changeset/shy-impalas-add.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/shy-impalas-add.md diff --git a/.changeset/shy-impalas-add.md b/.changeset/shy-impalas-add.md new file mode 100644 index 000000000000..e9f93508de5b --- /dev/null +++ b/.changeset/shy-impalas-add.md @@ -0,0 +1,5 @@ +--- +"next": patch +--- + +[TypeScript Plugin] Moved the diagnostics' positions to the prop's type instead of the value for client-boundary warnings.