Skip to content

Commit

Permalink
Port page and layout level API assertions to SWC transform (#40653)
Browse files Browse the repository at this point in the history
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)
  • Loading branch information
shuding committed Sep 19, 2022
1 parent c7e2619 commit c742c03
Show file tree
Hide file tree
Showing 10 changed files with 139 additions and 42 deletions.
103 changes: 101 additions & 2 deletions packages/next-swc/crates/core/src/react_server_components.rs
@@ -1,3 +1,4 @@
use regex::Regex;
use serde::Deserialize;

use swc_core::{
Expand Down Expand Up @@ -63,7 +64,7 @@ impl<C: Comments> VisitMut for ReactServerComponents<C> {
return;
}
} else {
self.assert_client_graph(&imports);
self.assert_client_graph(&imports, module);
}
module.visit_mut_children_with(self)
}
Expand Down Expand Up @@ -276,7 +277,7 @@ impl<C: Comments> ReactServerComponents<C> {
}
}

fn assert_client_graph(&self, imports: &Vec<ModuleImports>) {
fn assert_client_graph(&self, imports: &Vec<ModuleImports>, module: &Module) {
for import in imports {
let source = import.source.0.clone();
if self.invalid_client_imports.contains(&source) {
Expand All @@ -294,6 +295,104 @@ impl<C: Comments> ReactServerComponents<C> {
})
}
}

// 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()
})
}
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/next-swc/crates/core/tests/errors.rs
Expand Up @@ -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 },
),
Expand Down
@@ -0,0 +1,6 @@
export function getServerSideProps (){
}

export default function () {
return null;
}
@@ -0,0 +1,4 @@
export function getServerSideProps() {}
export default function() {
return null;
}
@@ -0,0 +1,6 @@

x `getServerSideProps` is not allowed in Client Components.
,-[input.js:1:1]
1 | export function getServerSideProps (){
: ^^^^^^^^^^^^^^^^^^
`----
@@ -0,0 +1,6 @@
export function getStaticProps (){
}

export default function () {
return null;
}
@@ -0,0 +1,4 @@
export function getStaticProps() {}
export default function() {
return null;
}
@@ -0,0 +1,6 @@

x `getStaticProps` is not allowed in Client Components.
,-[input.js:1:1]
1 | export function getStaticProps (){
: ^^^^^^^^^^^^^^
`----
36 changes: 1 addition & 35 deletions 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'

Expand All @@ -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,
Expand All @@ -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)
}

Expand Down
8 changes: 4 additions & 4 deletions test/e2e/app-dir/index.test.ts
Expand Up @@ -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)
Expand All @@ -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(
Expand All @@ -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'
)
})
}
Expand Down

0 comments on commit c742c03

Please sign in to comment.