Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Port page and layout level API assertions to SWC transform #40653

Merged
merged 4 commits into from Sep 19, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 (){
: ^^^^^^^^^^^^^^
`----
@@ -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 @@ -1061,7 +1061,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 @@ -1086,11 +1086,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 @@ -1113,7 +1113,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