From 9039a82bff535f7ff6db90846c407eb7067cfd09 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Thu, 3 Nov 2022 20:21:13 +0100 Subject: [PATCH 1/7] improve swc plugin --- .../core/src/react_server_components.rs | 70 +++++++++++++------ .../wellknown-errors-plugin/parseRSC.ts | 20 +++++- packages/next/taskfile-swc.js | 7 +- 3 files changed, 73 insertions(+), 24 deletions(-) diff --git a/packages/next-swc/crates/core/src/react_server_components.rs b/packages/next-swc/crates/core/src/react_server_components.rs index a5a1f4f431fb..78f4c2b62aa6 100644 --- a/packages/next-swc/crates/core/src/react_server_components.rs +++ b/packages/next-swc/crates/core/src/react_server_components.rs @@ -84,33 +84,63 @@ impl ReactServerComponents { let _ = &module.body.retain(|item| { match item { ModuleItem::Stmt(stmt) => { - if !finished_directives { - if !stmt.is_expr() { - // Not an expression. - finished_directives = true; - } + if !stmt.is_expr() { + // Not an expression. + finished_directives = true; + } - match stmt.as_expr() { - Some(expr_stmt) => { - match &*expr_stmt.expr { - Expr::Lit(Lit::Str(Str { value, .. })) => { - if &**value == "use client" { + match stmt.as_expr() { + Some(expr_stmt) => { + match &*expr_stmt.expr { + Expr::Lit(Lit::Str(Str { value, .. })) => { + if &**value == "use client" { + if !finished_directives { is_client_entry = true; - - // Remove the directive. - return false; + } else { + HANDLER.with(|handler| { + handler + .struct_span_err( + expr_stmt.span, + "NEXT_RSC_ERR_CLIENT_DIRECTIVE", + ) + .emit() + }) } + + // Remove the directive. + return false; } - _ => { - // Other expression types. - finished_directives = true; + } + // Match `ParenthesisExpression` which is some formartting tools + // usually do: ('use client'). In these case we need to throw + // an exception because they are not valid directives. + Expr::Paren(ParenExpr { expr, .. }) => { + finished_directives = true; + match &**expr { + Expr::Lit(Lit::Str(Str { value, .. })) => { + if &**value == "use client" { + HANDLER.with(|handler| { + handler + .struct_span_err( + expr_stmt.span, + "NEXT_RSC_ERR_CLIENT_DIRECTIVE_PAREN", + ) + .emit() + }) + } + } + _ => {} } } + _ => { + // Other expression types. + finished_directives = true; + } } - None => { - // Not an expression. - finished_directives = true; - } + } + None => { + // Not an expression. + finished_directives = true; } } } diff --git a/packages/next/build/webpack/plugins/wellknown-errors-plugin/parseRSC.ts b/packages/next/build/webpack/plugins/wellknown-errors-plugin/parseRSC.ts index 7b3a271a5a09..2fd483002860 100644 --- a/packages/next/build/webpack/plugins/wellknown-errors-plugin/parseRSC.ts +++ b/packages/next/build/webpack/plugins/wellknown-errors-plugin/parseRSC.ts @@ -3,9 +3,7 @@ import type { webpack } from 'next/dist/compiled/webpack/webpack' import { relative } from 'path' import { SimpleWebpackError } from './simpleWebpackError' -export function formatRSCErrorMessage( - message: string -): null | [string, string] { +function formatRSCErrorMessage(message: string): null | [string, string] { if (message && /NEXT_RSC_ERR_/.test(message)) { let formattedMessage = message let formattedVerboseMessage = '' @@ -15,6 +13,9 @@ export function formatRSCErrorMessage( const NEXT_RSC_ERR_REACT_API = /.+NEXT_RSC_ERR_REACT_API: (.*?)\n/s const NEXT_RSC_ERR_SERVER_IMPORT = /.+NEXT_RSC_ERR_SERVER_IMPORT: (.*?)\n/s const NEXT_RSC_ERR_CLIENT_IMPORT = /.+NEXT_RSC_ERR_CLIENT_IMPORT: (.*?)\n/s + const NEXT_RSC_ERR_CLIENT_DIRECTIVE = /.+NEXT_RSC_ERR_CLIENT_DIRECTIVE\n/s + const NEXT_RSC_ERR_CLIENT_DIRECTIVE_PAREN = + /.+NEXT_RSC_ERR_CLIENT_DIRECTIVE_PAREN\n/s if (NEXT_RSC_ERR_REACT_API.test(message)) { formattedMessage = message.replace( @@ -49,6 +50,18 @@ export function formatRSCErrorMessage( ) formattedVerboseMessage = '\n\nOne of these is marked as a client entry with "use client":\n' + } else if (NEXT_RSC_ERR_CLIENT_DIRECTIVE.test(message)) { + formattedMessage = message.replace( + NEXT_RSC_ERR_CLIENT_DIRECTIVE, + `\n\nThe "use client" directive must be placed before other expressions. Move it to the top of the file to resolve this issue.\n\n` + ) + formattedVerboseMessage = '\n\nImport path:\n' + } else if (NEXT_RSC_ERR_CLIENT_DIRECTIVE_PAREN.test(message)) { + formattedMessage = message.replace( + NEXT_RSC_ERR_CLIENT_DIRECTIVE_PAREN, + `\n\n"use client" must be a directive, and placed before other expressions. Remove the parentheses and move it to the top of the file to resolve this issue.\n\n` + ) + formattedVerboseMessage = '\n\nImport path:\n' } return [formattedMessage, formattedVerboseMessage] @@ -73,6 +86,7 @@ export function getRscError( // https://cs.github.com/webpack/webpack/blob/9fcaa243573005d6fdece9a3f8d89a0e8b399613/lib/stats/DefaultStatsFactoryPlugin.js#L414 const visitedModules = new Set() const moduleTrace = [] + let current = module while (current) { if (visitedModules.has(current)) break diff --git a/packages/next/taskfile-swc.js b/packages/next/taskfile-swc.js index 0f9e441822fc..50530e48c15b 100644 --- a/packages/next/taskfile-swc.js +++ b/packages/next/taskfile-swc.js @@ -117,7 +117,12 @@ module.exports = function (task) { // Make sure the output content keeps the `"use client"` directive. // TODO: Remove this once SWC fixes the issue. if (/^['"]use client['"]/.test(source)) { - output.code = '"use client";\n' + output.code + output.code = + '"use client";\n' + + output.code + .split('\n') + .map((l) => (/^['"]use client['"];$/.test(l) ? '' : l)) + .join('\n') } // Replace `.ts|.tsx` with `.js` in files with an extension From 7c183f6101ab6bb27054b1bacfc9a1446e2dee6c Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Thu, 3 Nov 2022 20:27:34 +0100 Subject: [PATCH 2/7] add test --- test/e2e/app-dir/rsc-errors.test.ts | 19 +++++++++++++++++++ .../rsc-errors/app/swc/use-client/page.js | 7 +++++++ 2 files changed, 26 insertions(+) create mode 100644 test/e2e/app-dir/rsc-errors/app/swc/use-client/page.js diff --git a/test/e2e/app-dir/rsc-errors.test.ts b/test/e2e/app-dir/rsc-errors.test.ts index e007b8b44798..4fde50468548 100644 --- a/test/e2e/app-dir/rsc-errors.test.ts +++ b/test/e2e/app-dir/rsc-errors.test.ts @@ -107,4 +107,23 @@ describe('app dir - rsc errors', () => { 'The default export is not a React Component in page: \\"/server-with-errors/page-export\\"' ) }) + + it('should throw an error when "use client" is on the top level but after other expressions', async () => { + const pageFile = 'app/swc/use-client/page.js' + const content = await next.readFile(pageFile) + const uncomment = content.replace("// 'use client'", "'use client'") + await next.patchFile(pageFile, uncomment) + const res = await fetchViaHTTP(next.url, '/swc/use-client') + await next.patchFile(pageFile, content) + + await check(async () => { + const { status } = await fetchViaHTTP(next.url, '/swc/use-client') + return status + }, /200/) + + expect(res.status).toBe(500) + expect(await res.text()).toContain( + 'directive must be placed before other expressions' + ) + }) }) diff --git a/test/e2e/app-dir/rsc-errors/app/swc/use-client/page.js b/test/e2e/app-dir/rsc-errors/app/swc/use-client/page.js new file mode 100644 index 000000000000..c1d79450e4a7 --- /dev/null +++ b/test/e2e/app-dir/rsc-errors/app/swc/use-client/page.js @@ -0,0 +1,7 @@ +import React from 'react' + +// 'use client' + +export default function Page() { + return 'hello' +} From 9e3564fb7ebd5c3cad3ba6bb69f52c2ad0693c6a Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Tue, 8 Nov 2022 23:37:46 +0100 Subject: [PATCH 3/7] fix lint error --- packages/next/taskfile-swc.js | 2 +- test/e2e/app-dir/rsc-errors/app/swc/use-client/page.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/next/taskfile-swc.js b/packages/next/taskfile-swc.js index 50530e48c15b..43652e0b0676 100644 --- a/packages/next/taskfile-swc.js +++ b/packages/next/taskfile-swc.js @@ -121,7 +121,7 @@ module.exports = function (task) { '"use client";\n' + output.code .split('\n') - .map((l) => (/^['"]use client['"];$/.test(l) ? '' : l)) + .map((l) => (/^['"]use client['"]/.test(l) ? '' : l)) .join('\n') } diff --git a/test/e2e/app-dir/rsc-errors/app/swc/use-client/page.js b/test/e2e/app-dir/rsc-errors/app/swc/use-client/page.js index c1d79450e4a7..c5a835318db6 100644 --- a/test/e2e/app-dir/rsc-errors/app/swc/use-client/page.js +++ b/test/e2e/app-dir/rsc-errors/app/swc/use-client/page.js @@ -1,4 +1,4 @@ -import React from 'react' +import 'react' // 'use client' From 0e9b2c00d5c539f89297efe13800ed3e0573b354 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Tue, 8 Nov 2022 23:49:50 +0100 Subject: [PATCH 4/7] refactor plugin --- .../core/src/react_server_components.rs | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/packages/next-swc/crates/core/src/react_server_components.rs b/packages/next-swc/crates/core/src/react_server_components.rs index 78f4c2b62aa6..45a52f3bbe35 100644 --- a/packages/next-swc/crates/core/src/react_server_components.rs +++ b/packages/next-swc/crates/core/src/react_server_components.rs @@ -116,20 +116,17 @@ impl ReactServerComponents { // an exception because they are not valid directives. Expr::Paren(ParenExpr { expr, .. }) => { finished_directives = true; - match &**expr { - Expr::Lit(Lit::Str(Str { value, .. })) => { - if &**value == "use client" { - HANDLER.with(|handler| { - handler - .struct_span_err( - expr_stmt.span, - "NEXT_RSC_ERR_CLIENT_DIRECTIVE_PAREN", - ) - .emit() - }) - } + if let Expr::Lit(Lit::Str(Str { value, .. })) = &**expr { + if &**value == "use client" { + HANDLER.with(|handler| { + handler + .struct_span_err( + expr_stmt.span, + "NEXT_RSC_ERR_CLIENT_DIRECTIVE_PAREN", + ) + .emit() + }) } - _ => {} } } _ => { From 5fd066eb244cb834eaf1a1336f8c141809ef9f1d Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Tue, 8 Nov 2022 23:57:49 +0100 Subject: [PATCH 5/7] add test cases --- .../client-graph/use-client/input.js | 7 +++++++ .../client-graph/use-client/output.js | 4 ++++ .../client-graph/use-client/output.stderr | 6 ++++++ .../client-graph/client-entry/input.js | 2 -- .../client-graph/client-entry/output.js | 1 - 5 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/use-client/input.js create mode 100644 packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/use-client/output.js create mode 100644 packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/use-client/output.stderr diff --git a/packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/use-client/input.js b/packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/use-client/input.js new file mode 100644 index 000000000000..8086d0bb60a0 --- /dev/null +++ b/packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/use-client/input.js @@ -0,0 +1,7 @@ +import "react" + +"use client" + +export default function () { + return null; +} diff --git a/packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/use-client/output.js b/packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/use-client/output.js new file mode 100644 index 000000000000..da774a566f24 --- /dev/null +++ b/packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/use-client/output.js @@ -0,0 +1,4 @@ +import "react"; +export default function () { + return null; +} diff --git a/packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/use-client/output.stderr b/packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/use-client/output.stderr new file mode 100644 index 000000000000..8d9ba60ea5a1 --- /dev/null +++ b/packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/use-client/output.stderr @@ -0,0 +1,6 @@ + + x NEXT_RSC_ERR_CLIENT_DIRECTIVE + ,-[input.js:3:1] + 3 | "use client" + : ^^^^^^^^^^^^ + `---- diff --git a/packages/next-swc/crates/core/tests/fixture/react-server-components/client-graph/client-entry/input.js b/packages/next-swc/crates/core/tests/fixture/react-server-components/client-graph/client-entry/input.js index a0d5630f960c..41d166d30acc 100644 --- a/packages/next-swc/crates/core/tests/fixture/react-server-components/client-graph/client-entry/input.js +++ b/packages/next-swc/crates/core/tests/fixture/react-server-components/client-graph/client-entry/input.js @@ -16,8 +16,6 @@ import "fs" -"use client"; - "bar"; // This is a comment. diff --git a/packages/next-swc/crates/core/tests/fixture/react-server-components/client-graph/client-entry/output.js b/packages/next-swc/crates/core/tests/fixture/react-server-components/client-graph/client-entry/output.js index 6c655b686fd7..d6f4baeace57 100644 --- a/packages/next-swc/crates/core/tests/fixture/react-server-components/client-graph/client-entry/output.js +++ b/packages/next-swc/crates/core/tests/fixture/react-server-components/client-graph/client-entry/output.js @@ -3,7 +3,6 @@ // This is a comment. "foo"; import "fs"; -"use client"; "bar"; // This is a comment. 1 + 1; From a6f2b1a4df34b816acd3d50448b0dfc437035f37 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Thu, 10 Nov 2022 15:42:45 +0100 Subject: [PATCH 6/7] fix lint error --- test/e2e/app-dir/rsc-errors/app/swc/use-client/page.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/app-dir/rsc-errors/app/swc/use-client/page.js b/test/e2e/app-dir/rsc-errors/app/swc/use-client/page.js index c5a835318db6..c1d79450e4a7 100644 --- a/test/e2e/app-dir/rsc-errors/app/swc/use-client/page.js +++ b/test/e2e/app-dir/rsc-errors/app/swc/use-client/page.js @@ -1,4 +1,4 @@ -import 'react' +import React from 'react' // 'use client' From aa336ef7678a689cb3528d37c015aaf9a848cd97 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Fri, 11 Nov 2022 16:45:06 -0800 Subject: [PATCH 7/7] fix lint --- .eslintignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.eslintignore b/.eslintignore index 7e00238ebef0..abb224fcce93 100644 --- a/.eslintignore +++ b/.eslintignore @@ -29,6 +29,7 @@ test/integration/eslint/** test/integration/script-loader/**/* test/development/basic/legacy-decorators/**/* test/production/emit-decorator-metadata/**/*.js +test/e2e/app-dir/rsc-errors/app/swc/use-client/page.js test-timings.json packages/next-swc/crates/** bench/nested-deps/pages/**