From 706cdfd4f2942cceb8e669d876778ba04fb8f151 Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Fri, 6 Sep 2024 10:32:31 +0200 Subject: [PATCH] Fix regression in TS plugin: allow `reset` prop in error files The TypeScript error `Props must be serializable for components in the "use client" entry file, "reset" is invalid.` for the `reset` prop in `error.tsx` and `global-error.tsx` was fixed in #46898 and #48756, respectively. However, a regression of the fix was accidentally introduced in #67211. This PR restores the previous behavior and adds fixtures for manual testing. (Since the Next.js TS plugin only applies to VSCode, manual testing in VSCode is required. See `test/development/typescript-plugin/README.md` for details.) --- .../typescript/rules/client-boundary.ts | 40 +++++++++++-------- .../typescript-plugin/app/client.tsx | 12 +++++- .../typescript-plugin/app/error.tsx | 23 +++++++++++ .../typescript-plugin/app/global-error.tsx | 19 +++++++++ 4 files changed, 75 insertions(+), 19 deletions(-) create mode 100644 test/development/typescript-plugin/app/error.tsx create mode 100644 test/development/typescript-plugin/app/global-error.tsx diff --git a/packages/next/src/server/typescript/rules/client-boundary.ts b/packages/next/src/server/typescript/rules/client-boundary.ts index 9ed1616a88ac98..5a6c2a3773e581 100644 --- a/packages/next/src/server/typescript/rules/client-boundary.ts +++ b/packages/next/src/server/typescript/rules/client-boundary.ts @@ -52,12 +52,23 @@ const clientBoundary = { 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. + // 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) - if (!maybeServerAction) { + + // 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, @@ -75,19 +86,14 @@ const clientBoundary = { ts.isConstructorTypeNode(typeDeclarationNode) || ts.isClassDeclaration(typeDeclarationNode) ) { - // 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 - if (!(isErrorFile || isGlobalErrorFile) || propName !== 'reset') { - 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(), - }) - } + 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(), + }) } } } diff --git a/test/development/typescript-plugin/app/client.tsx b/test/development/typescript-plugin/app/client.tsx index 13b5f542ac9d67..b4969313507e6c 100644 --- a/test/development/typescript-plugin/app/client.tsx +++ b/test/development/typescript-plugin/app/client.tsx @@ -1,14 +1,22 @@ 'use client' +class MyClass {} + export function ClientComponent({ unknownAction, //^^^^^^^^^^^ fine because it looks like an action unknown, - //^^^^^ "Error(TS71007): Props must be serializable for components in the "use client" entry file. "unknown" is a function that's not a Server Action. Rename "unknown" either to "action" or have its name end with "Action" e.g. "unknownAction" to indicate it is a Server Action.ts(71007) + //^^^^^ "Error(TS71007): Props must be serializable for components in the "use client" entry file. "unknown" is a function that's not a Server Action. Rename "unknown" either to "action" or have its name end with "Action" e.g. "unknownAction" to indicate it is a Server Action. ts(71007) + foo, + //^ "Error(TS71007): Props must be serializable for components in the "use client" entry file, "foo" is invalid.ts(71007) + bar, + //^ "Error(TS71007): Props must be serializable for components in the "use client" entry file, "bar" is invalid.ts(71007) }: { unknownAction: () => void unknown: () => void + foo: new () => Error + bar: MyClass }) { - console.log({ unknown, unknownAction }) + console.log({ unknown, unknownAction, foo, bar }) return null } diff --git a/test/development/typescript-plugin/app/error.tsx b/test/development/typescript-plugin/app/error.tsx new file mode 100644 index 00000000000000..536b1161e79e61 --- /dev/null +++ b/test/development/typescript-plugin/app/error.tsx @@ -0,0 +1,23 @@ +'use client' + +import { useEffect } from 'react' + +export default function Error({ + error, + reset, + //^^^ fine because it's the special reset prop in an error file +}: { + error: Error & { digest?: string } + reset: () => void +}) { + useEffect(() => { + console.error(error) + }, [error]) + + return ( +
+

Something went wrong!

+ +
+ ) +} diff --git a/test/development/typescript-plugin/app/global-error.tsx b/test/development/typescript-plugin/app/global-error.tsx new file mode 100644 index 00000000000000..2852c1f02047dc --- /dev/null +++ b/test/development/typescript-plugin/app/global-error.tsx @@ -0,0 +1,19 @@ +'use client' + +export default function GlobalError({ + error, + reset, + //^^^ fine because it's the special reset prop in a global-error file +}: { + error: Error & { digest?: string } + reset: () => void +}) { + return ( + + +

Something went wrong!

+ + + + ) +}