From c742c038b0b597b10767a066e9d03ac280fe65f5 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Mon, 19 Sep 2022 14:30:32 +0200 Subject: [PATCH] Port page and layout level API assertions to SWC transform (#40653) We used to do an extra pass of SWR `parse` and loop over the AST inside JavaScript to check if `getServerSideProps` or `getStaticProps` is used in a client page or layout. Instead this can be done in the same `react_server_components` SWC transform now. ## Bug - [ ] Related issues linked using `fixes #number` - [x] Integration tests added - [ ] Errors have a helpful link attached, see `contributing.md` ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have a helpful link attached, see `contributing.md` ## Documentation / Examples - [ ] Make sure the linting passes by running `pnpm lint` - [ ] The "examples guidelines" are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md) --- .../core/src/react_server_components.rs | 103 +++++++++++++++++- packages/next-swc/crates/core/tests/errors.rs | 2 +- .../get-server-side-props/input.js | 6 + .../get-server-side-props/output.js | 4 + .../get-server-side-props/output.stderr | 6 + .../client-graph/get-static-props/input.js | 6 + .../client-graph/get-static-props/output.js | 4 + .../get-static-props/output.stderr | 6 + .../loaders/next-flight-loader/index.ts | 36 +----- test/e2e/app-dir/index.test.ts | 8 +- 10 files changed, 139 insertions(+), 42 deletions(-) create mode 100644 packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/get-server-side-props/input.js create mode 100644 packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/get-server-side-props/output.js create mode 100644 packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/get-server-side-props/output.stderr create mode 100644 packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/get-static-props/input.js create mode 100644 packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/get-static-props/output.js create mode 100644 packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/get-static-props/output.stderr 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 b172ea47fc4ed7c..db5b5c01ac6a4a1 100644 --- a/packages/next-swc/crates/core/src/react_server_components.rs +++ b/packages/next-swc/crates/core/src/react_server_components.rs @@ -1,3 +1,4 @@ +use regex::Regex; use serde::Deserialize; use swc_core::{ @@ -63,7 +64,7 @@ impl VisitMut for ReactServerComponents { return; } } else { - self.assert_client_graph(&imports); + self.assert_client_graph(&imports, module); } module.visit_mut_children_with(self) } @@ -276,7 +277,7 @@ impl ReactServerComponents { } } - fn assert_client_graph(&self, imports: &Vec) { + fn assert_client_graph(&self, imports: &Vec, module: &Module) { for import in imports { let source = import.source.0.clone(); if self.invalid_client_imports.contains(&source) { @@ -294,6 +295,104 @@ impl ReactServerComponents { }) } } + + // Assert `getServerSideProps` and `getStaticProps` exports. + let is_layout_or_page = Regex::new(r"/(page|layout)\.(ts|js)x?$") + .unwrap() + .is_match(&self.filepath); + if is_layout_or_page { + let mut span = DUMMY_SP; + let mut has_get_server_side_props = false; + let mut has_get_static_props = false; + + 'matcher: for export in &module.body { + match export { + ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(export)) => { + for specifier in &export.specifiers { + if let ExportSpecifier::Named(named) = specifier { + match &named.orig { + ModuleExportName::Ident(i) => { + if i.sym == *"getServerSideProps" { + has_get_server_side_props = true; + span = named.span; + break 'matcher; + } + if i.sym == *"getStaticProps" { + has_get_static_props = true; + span = named.span; + break 'matcher; + } + } + ModuleExportName::Str(s) => { + if s.value == *"getServerSideProps" { + has_get_server_side_props = true; + span = named.span; + break 'matcher; + } + if s.value == *"getStaticProps" { + has_get_static_props = true; + span = named.span; + break 'matcher; + } + } + } + } + } + } + ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export)) => match &export.decl { + Decl::Fn(f) => { + if f.ident.sym == *"getServerSideProps" { + has_get_server_side_props = true; + span = f.ident.span; + break 'matcher; + } + if f.ident.sym == *"getStaticProps" { + has_get_static_props = true; + span = f.ident.span; + break 'matcher; + } + } + Decl::Var(v) => { + for decl in &v.decls { + if let Pat::Ident(i) = &decl.name { + if i.sym == *"getServerSideProps" { + has_get_server_side_props = true; + span = i.span; + break 'matcher; + } + if i.sym == *"getStaticProps" { + has_get_static_props = true; + span = i.span; + break 'matcher; + } + } + } + } + _ => {} + }, + _ => {} + } + } + + if has_get_server_side_props || has_get_static_props { + HANDLER.with(|handler| { + handler + .struct_span_err( + span, + format!( + "`{}` is not allowed in Client Components.", + if has_get_server_side_props { + "getServerSideProps" + } else { + "getStaticProps" + } + ) + .as_str(), + ) + .emit() + }) + } + } } } diff --git a/packages/next-swc/crates/core/tests/errors.rs b/packages/next-swc/crates/core/tests/errors.rs index 3b6996807e5d19b..5571406e6a22b2c 100644 --- a/packages/next-swc/crates/core/tests/errors.rs +++ b/packages/next-swc/crates/core/tests/errors.rs @@ -83,7 +83,7 @@ fn react_server_components_client_graph_errors(input: PathBuf) { syntax(), &|tr| { server_components( - FileName::Real(PathBuf::from("/some-project/src/some-file.js")), + FileName::Real(PathBuf::from("/some-project/src/page.js")), next_swc::react_server_components::Config::WithOptions( next_swc::react_server_components::Options { is_server: false }, ), diff --git a/packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/get-server-side-props/input.js b/packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/get-server-side-props/input.js new file mode 100644 index 000000000000000..54ca0a245d2fdf2 --- /dev/null +++ b/packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/get-server-side-props/input.js @@ -0,0 +1,6 @@ +export function getServerSideProps (){ +} + +export default function () { + return null; +} diff --git a/packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/get-server-side-props/output.js b/packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/get-server-side-props/output.js new file mode 100644 index 000000000000000..c2a54dc35918f42 --- /dev/null +++ b/packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/get-server-side-props/output.js @@ -0,0 +1,4 @@ +export function getServerSideProps() {} +export default function() { + return null; +} diff --git a/packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/get-server-side-props/output.stderr b/packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/get-server-side-props/output.stderr new file mode 100644 index 000000000000000..8c649c3e39f8a4c --- /dev/null +++ b/packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/get-server-side-props/output.stderr @@ -0,0 +1,6 @@ + + x `getServerSideProps` is not allowed in Client Components. + ,-[input.js:1:1] + 1 | export function getServerSideProps (){ + : ^^^^^^^^^^^^^^^^^^ + `---- diff --git a/packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/get-static-props/input.js b/packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/get-static-props/input.js new file mode 100644 index 000000000000000..7b8b6b06871d53c --- /dev/null +++ b/packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/get-static-props/input.js @@ -0,0 +1,6 @@ +export function getStaticProps (){ +} + +export default function () { + return null; +} diff --git a/packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/get-static-props/output.js b/packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/get-static-props/output.js new file mode 100644 index 000000000000000..26b9fcd3f86bce3 --- /dev/null +++ b/packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/get-static-props/output.js @@ -0,0 +1,4 @@ +export function getStaticProps() {} +export default function() { + return null; +} diff --git a/packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/get-static-props/output.stderr b/packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/get-static-props/output.stderr new file mode 100644 index 000000000000000..67cae2d6ee7892a --- /dev/null +++ b/packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/get-static-props/output.stderr @@ -0,0 +1,6 @@ + + x `getStaticProps` is not allowed in Client Components. + ,-[input.js:1:1] + 1 | export function getStaticProps (){ + : ^^^^^^^^^^^^^^ + `---- diff --git a/packages/next/build/webpack/loaders/next-flight-loader/index.ts b/packages/next/build/webpack/loaders/next-flight-loader/index.ts index c05e8ea4b43e6ec..1f938f1256a03fd 100644 --- a/packages/next/build/webpack/loaders/next-flight-loader/index.ts +++ b/packages/next/build/webpack/loaders/next-flight-loader/index.ts @@ -1,9 +1,5 @@ -import path from 'path' import { RSC_MODULE_TYPES } from '../../../../shared/lib/constants' -import { - checkExports, - getRSCModuleType, -} from '../../../analysis/get-page-static-info' +import { getRSCModuleType } from '../../../analysis/get-page-static-info' import { parse } from '../../../swc' import { getModuleBuildInfo } from '../get-module-build-info' @@ -15,16 +11,6 @@ function transformServer(source: string, isESModule: boolean) { ) } -function containsPath(parent: string, child: string) { - const relation = path.relative(parent, child) - return !!relation && !relation.startsWith('..') && !path.isAbsolute(relation) -} - -const isPageOrLayoutFile = (filePath: string) => { - const filename = path.basename(filePath) - return /[\\/]?(page|layout)\.(js|ts)x?$/.test(filename) -} - export default async function transformSource( this: any, source: string, @@ -44,32 +30,12 @@ export default async function transformSource( }) const rscType = getRSCModuleType(source) - const createError = (name: string) => - new Error( - `${name} is not supported in client components.\nFrom: ${this.resourcePath}` - ) - const appDir = path.join(this.rootContext, 'app') - const isUnderAppDir = containsPath(appDir, this.resourcePath) - const isResourcePageOrLayoutFile = isPageOrLayoutFile(this.resourcePath) - // If client entry has any gSSP/gSP data fetching methods, error - function errorForInvalidDataFetching(onError: (error: any) => void) { - if (isUnderAppDir && isResourcePageOrLayoutFile) { - const { ssg, ssr } = checkExports(swcAST) - if (ssg) { - onError(createError('getStaticProps')) - } - if (ssr) { - onError(createError('getServerSideProps')) - } - } - } // Assign the RSC meta information to buildInfo. // Exclude next internal files which are not marked as client files buildInfo.rsc = { type: rscType } if (buildInfo.rsc?.type === RSC_MODULE_TYPES.client) { - errorForInvalidDataFetching(this.emitError) return callback(null, source, sourceMap) } diff --git a/test/e2e/app-dir/index.test.ts b/test/e2e/app-dir/index.test.ts index fcc5c80c7adeac3..10efc212720a438 100644 --- a/test/e2e/app-dir/index.test.ts +++ b/test/e2e/app-dir/index.test.ts @@ -1077,7 +1077,7 @@ describe('app dir', () => { }) if (isDev) { - it.skip('should throw an error when getServerSideProps is used', async () => { + it('should throw an error when getServerSideProps is used', async () => { const pageFile = 'app/client-with-errors/get-server-side-props/page.js' const content = await next.readFile(pageFile) @@ -1102,11 +1102,11 @@ describe('app dir', () => { expect(res.status).toBe(500) expect(await res.text()).toContain( - 'getServerSideProps is not supported in client components' + '`getServerSideProps` is not allowed in Client Components' ) }) - it.skip('should throw an error when getStaticProps is used', async () => { + it('should throw an error when getStaticProps is used', async () => { const pageFile = 'app/client-with-errors/get-static-props/page.js' const content = await next.readFile(pageFile) const uncomment = content.replace( @@ -1129,7 +1129,7 @@ describe('app dir', () => { expect(res.status).toBe(500) expect(await res.text()).toContain( - 'getStaticProps is not supported in client components' + '`getStaticProps` is not allowed in Client Components' ) }) }