Skip to content

Commit c742c03

Browse files
authored
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)
1 parent c7e2619 commit c742c03

File tree

10 files changed

+139
-42
lines changed

10 files changed

+139
-42
lines changed

packages/next-swc/crates/core/src/react_server_components.rs

Lines changed: 101 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use regex::Regex;
12
use serde::Deserialize;
23

34
use swc_core::{
@@ -63,7 +64,7 @@ impl<C: Comments> VisitMut for ReactServerComponents<C> {
6364
return;
6465
}
6566
} else {
66-
self.assert_client_graph(&imports);
67+
self.assert_client_graph(&imports, module);
6768
}
6869
module.visit_mut_children_with(self)
6970
}
@@ -276,7 +277,7 @@ impl<C: Comments> ReactServerComponents<C> {
276277
}
277278
}
278279

279-
fn assert_client_graph(&self, imports: &Vec<ModuleImports>) {
280+
fn assert_client_graph(&self, imports: &Vec<ModuleImports>, module: &Module) {
280281
for import in imports {
281282
let source = import.source.0.clone();
282283
if self.invalid_client_imports.contains(&source) {
@@ -294,6 +295,104 @@ impl<C: Comments> ReactServerComponents<C> {
294295
})
295296
}
296297
}
298+
299+
// Assert `getServerSideProps` and `getStaticProps` exports.
300+
let is_layout_or_page = Regex::new(r"/(page|layout)\.(ts|js)x?$")
301+
.unwrap()
302+
.is_match(&self.filepath);
303+
if is_layout_or_page {
304+
let mut span = DUMMY_SP;
305+
let mut has_get_server_side_props = false;
306+
let mut has_get_static_props = false;
307+
308+
'matcher: for export in &module.body {
309+
match export {
310+
ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(export)) => {
311+
for specifier in &export.specifiers {
312+
if let ExportSpecifier::Named(named) = specifier {
313+
match &named.orig {
314+
ModuleExportName::Ident(i) => {
315+
if i.sym == *"getServerSideProps" {
316+
has_get_server_side_props = true;
317+
span = named.span;
318+
break 'matcher;
319+
}
320+
if i.sym == *"getStaticProps" {
321+
has_get_static_props = true;
322+
span = named.span;
323+
break 'matcher;
324+
}
325+
}
326+
ModuleExportName::Str(s) => {
327+
if s.value == *"getServerSideProps" {
328+
has_get_server_side_props = true;
329+
span = named.span;
330+
break 'matcher;
331+
}
332+
if s.value == *"getStaticProps" {
333+
has_get_static_props = true;
334+
span = named.span;
335+
break 'matcher;
336+
}
337+
}
338+
}
339+
}
340+
}
341+
}
342+
ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export)) => match &export.decl {
343+
Decl::Fn(f) => {
344+
if f.ident.sym == *"getServerSideProps" {
345+
has_get_server_side_props = true;
346+
span = f.ident.span;
347+
break 'matcher;
348+
}
349+
if f.ident.sym == *"getStaticProps" {
350+
has_get_static_props = true;
351+
span = f.ident.span;
352+
break 'matcher;
353+
}
354+
}
355+
Decl::Var(v) => {
356+
for decl in &v.decls {
357+
if let Pat::Ident(i) = &decl.name {
358+
if i.sym == *"getServerSideProps" {
359+
has_get_server_side_props = true;
360+
span = i.span;
361+
break 'matcher;
362+
}
363+
if i.sym == *"getStaticProps" {
364+
has_get_static_props = true;
365+
span = i.span;
366+
break 'matcher;
367+
}
368+
}
369+
}
370+
}
371+
_ => {}
372+
},
373+
_ => {}
374+
}
375+
}
376+
377+
if has_get_server_side_props || has_get_static_props {
378+
HANDLER.with(|handler| {
379+
handler
380+
.struct_span_err(
381+
span,
382+
format!(
383+
"`{}` is not allowed in Client Components.",
384+
if has_get_server_side_props {
385+
"getServerSideProps"
386+
} else {
387+
"getStaticProps"
388+
}
389+
)
390+
.as_str(),
391+
)
392+
.emit()
393+
})
394+
}
395+
}
297396
}
298397
}
299398

packages/next-swc/crates/core/tests/errors.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ fn react_server_components_client_graph_errors(input: PathBuf) {
8383
syntax(),
8484
&|tr| {
8585
server_components(
86-
FileName::Real(PathBuf::from("/some-project/src/some-file.js")),
86+
FileName::Real(PathBuf::from("/some-project/src/page.js")),
8787
next_swc::react_server_components::Config::WithOptions(
8888
next_swc::react_server_components::Options { is_server: false },
8989
),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export function getServerSideProps (){
2+
}
3+
4+
export default function () {
5+
return null;
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export function getServerSideProps() {}
2+
export default function() {
3+
return null;
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
2+
x `getServerSideProps` is not allowed in Client Components.
3+
,-[input.js:1:1]
4+
1 | export function getServerSideProps (){
5+
: ^^^^^^^^^^^^^^^^^^
6+
`----
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export function getStaticProps (){
2+
}
3+
4+
export default function () {
5+
return null;
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export function getStaticProps() {}
2+
export default function() {
3+
return null;
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
2+
x `getStaticProps` is not allowed in Client Components.
3+
,-[input.js:1:1]
4+
1 | export function getStaticProps (){
5+
: ^^^^^^^^^^^^^^
6+
`----

packages/next/build/webpack/loaders/next-flight-loader/index.ts

Lines changed: 1 addition & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
1-
import path from 'path'
21
import { RSC_MODULE_TYPES } from '../../../../shared/lib/constants'
3-
import {
4-
checkExports,
5-
getRSCModuleType,
6-
} from '../../../analysis/get-page-static-info'
2+
import { getRSCModuleType } from '../../../analysis/get-page-static-info'
73
import { parse } from '../../../swc'
84
import { getModuleBuildInfo } from '../get-module-build-info'
95

@@ -15,16 +11,6 @@ function transformServer(source: string, isESModule: boolean) {
1511
)
1612
}
1713

18-
function containsPath(parent: string, child: string) {
19-
const relation = path.relative(parent, child)
20-
return !!relation && !relation.startsWith('..') && !path.isAbsolute(relation)
21-
}
22-
23-
const isPageOrLayoutFile = (filePath: string) => {
24-
const filename = path.basename(filePath)
25-
return /[\\/]?(page|layout)\.(js|ts)x?$/.test(filename)
26-
}
27-
2814
export default async function transformSource(
2915
this: any,
3016
source: string,
@@ -44,32 +30,12 @@ export default async function transformSource(
4430
})
4531

4632
const rscType = getRSCModuleType(source)
47-
const createError = (name: string) =>
48-
new Error(
49-
`${name} is not supported in client components.\nFrom: ${this.resourcePath}`
50-
)
51-
const appDir = path.join(this.rootContext, 'app')
52-
const isUnderAppDir = containsPath(appDir, this.resourcePath)
53-
const isResourcePageOrLayoutFile = isPageOrLayoutFile(this.resourcePath)
54-
// If client entry has any gSSP/gSP data fetching methods, error
55-
function errorForInvalidDataFetching(onError: (error: any) => void) {
56-
if (isUnderAppDir && isResourcePageOrLayoutFile) {
57-
const { ssg, ssr } = checkExports(swcAST)
58-
if (ssg) {
59-
onError(createError('getStaticProps'))
60-
}
61-
if (ssr) {
62-
onError(createError('getServerSideProps'))
63-
}
64-
}
65-
}
6633

6734
// Assign the RSC meta information to buildInfo.
6835
// Exclude next internal files which are not marked as client files
6936
buildInfo.rsc = { type: rscType }
7037

7138
if (buildInfo.rsc?.type === RSC_MODULE_TYPES.client) {
72-
errorForInvalidDataFetching(this.emitError)
7339
return callback(null, source, sourceMap)
7440
}
7541

test/e2e/app-dir/index.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1077,7 +1077,7 @@ describe('app dir', () => {
10771077
})
10781078

10791079
if (isDev) {
1080-
it.skip('should throw an error when getServerSideProps is used', async () => {
1080+
it('should throw an error when getServerSideProps is used', async () => {
10811081
const pageFile =
10821082
'app/client-with-errors/get-server-side-props/page.js'
10831083
const content = await next.readFile(pageFile)
@@ -1102,11 +1102,11 @@ describe('app dir', () => {
11021102

11031103
expect(res.status).toBe(500)
11041104
expect(await res.text()).toContain(
1105-
'getServerSideProps is not supported in client components'
1105+
'`getServerSideProps` is not allowed in Client Components'
11061106
)
11071107
})
11081108

1109-
it.skip('should throw an error when getStaticProps is used', async () => {
1109+
it('should throw an error when getStaticProps is used', async () => {
11101110
const pageFile = 'app/client-with-errors/get-static-props/page.js'
11111111
const content = await next.readFile(pageFile)
11121112
const uncomment = content.replace(
@@ -1129,7 +1129,7 @@ describe('app dir', () => {
11291129

11301130
expect(res.status).toBe(500)
11311131
expect(await res.text()).toContain(
1132-
'getStaticProps is not supported in client components'
1132+
'`getStaticProps` is not allowed in Client Components'
11331133
)
11341134
})
11351135
}

0 commit comments

Comments
 (0)