Skip to content

Commit

Permalink
Fix export statement validations for app router pages (#75278)
Browse files Browse the repository at this point in the history
Fixes all issues related to the current export statement validation for
app router pages, as highlighted in #75277.
  • Loading branch information
unstubbable authored Jan 24, 2025
1 parent eadb566 commit bc3e597
Show file tree
Hide file tree
Showing 6 changed files with 112 additions and 109 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/next-custom-transforms/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ easy-error = "1.0.0"
either = "1"
fxhash = "0.2.1"
hex = "0.4.3"
indexmap = { workspace = true }
indoc = { workspace = true }
once_cell = { workspace = true }
pathdiff = { workspace = true }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::{collections::HashMap, path::PathBuf, rc::Rc, sync::Arc};

use indexmap::IndexMap;
use once_cell::sync::Lazy;
use regex::Regex;
use serde::Deserialize;
Expand Down Expand Up @@ -75,7 +76,7 @@ enum RSCErrorKind {
NextRscErrReactApi((String, Span)),
NextRscErrErrorFileServerComponent(Span),
NextRscErrClientMetadataExport((String, Span)),
NextRscErrConflictMetadataExport(Span),
NextRscErrConflictMetadataExport((Span, Span)),
NextRscErrInvalidApi((String, Span)),
NextRscErrDeprecatedApi((String, String, Span)),
NextSsrDynamicFalseNotAllowed(Span),
Expand All @@ -84,6 +85,7 @@ enum RSCErrorKind {

enum InvalidExportKind {
General,
Metadata,
DynamicIoSegment,
}

Expand Down Expand Up @@ -233,18 +235,18 @@ impl<C: Comments> ReactServerComponents<C> {
/// Consolidated place to parse, generate error messages for the RSC parsing
/// errors.
fn report_error(app_dir: &Option<PathBuf>, filepath: &str, error_kind: RSCErrorKind) {
let (msg, span) = match error_kind {
let (msg, spans) = match error_kind {
RSCErrorKind::RedundantDirectives(span) => (
"It's not possible to have both `use client` and `use server` directives in the \
same file."
.to_string(),
span,
vec![span],
),
RSCErrorKind::NextRscErrClientDirective(span) => (
"The \"use client\" directive must be placed before other expressions. Move it to \
the top of the file to resolve this issue."
.to_string(),
span,
vec![span],
),
RSCErrorKind::NextRscErrServerImport((source, span)) => {
let msg = match source.as_str() {
Expand All @@ -255,7 +257,7 @@ fn report_error(app_dir: &Option<PathBuf>, filepath: &str, error_kind: RSCErrorK
_ => format!(r#"You're importing a component that imports {source}. It only works in a Client Component but none of its parents are marked with "use client", so they're Server Components by default.\nLearn more: https://nextjs.org/docs/app/building-your-application/rendering\n\n"#)
};

(msg, span)
(msg, vec![span])
}
RSCErrorKind::NextRscErrClientImport((source, span)) => {
let is_app_dir = app_dir
Expand All @@ -274,7 +276,7 @@ fn report_error(app_dir: &Option<PathBuf>, filepath: &str, error_kind: RSCErrorK
} else {
format!("You're importing a component that needs \"{source}\". That only works in a Server Component but one of its parents is marked with \"use client\", so it's a Client Component.\nLearn more: https://nextjs.org/docs/app/building-your-application/rendering\n\n")
};
(msg, span)
(msg, vec![span])
}
RSCErrorKind::NextRscErrReactApi((source, span)) => {
let msg = if source == "Component" {
Expand All @@ -283,46 +285,46 @@ fn report_error(app_dir: &Option<PathBuf>, filepath: &str, error_kind: RSCErrorK
format!("You're importing a component that needs `{source}`. This React hook only works in a client component. To fix, mark the file (or its parent) with the `\"use client\"` directive.\n\n Learn more: https://nextjs.org/docs/app/api-reference/directives/use-client\n\n")
};

(msg,span)
(msg, vec![span])
},
RSCErrorKind::NextRscErrErrorFileServerComponent(span) => {
(
format!("{filepath} must be a Client Component. Add the \"use client\" directive the top of the file to resolve this issue.\nLearn more: https://nextjs.org/docs/app/api-reference/directives/use-client\n\n"),
span
vec![span]
)
},
RSCErrorKind::NextRscErrClientMetadataExport((source, span)) => {
(format!("You are attempting to export \"{source}\" from a component marked with \"use client\", which is disallowed. Either remove the export, or the \"use client\" directive. Read more: https://nextjs.org/docs/app/api-reference/directives/use-client\n\n"), span)
(format!("You are attempting to export \"{source}\" from a component marked with \"use client\", which is disallowed. Either remove the export, or the \"use client\" directive. Read more: https://nextjs.org/docs/app/api-reference/directives/use-client\n\n"), vec![span])
},
RSCErrorKind::NextRscErrConflictMetadataExport(span) => (
RSCErrorKind::NextRscErrConflictMetadataExport((span1, span2)) => (
"\"metadata\" and \"generateMetadata\" cannot be exported at the same time, please keep one of them. Read more: https://nextjs.org/docs/app/api-reference/file-conventions/metadata\n\n".to_string(),
span
vec![span1, span2]
),
//NEXT_RSC_ERR_INVALID_API
RSCErrorKind::NextRscErrInvalidApi((source, span)) => (
format!("\"{source}\" is not supported in app/. Read more: https://nextjs.org/docs/app/building-your-application/data-fetching\n\n"), span
format!("\"{source}\" is not supported in app/. Read more: https://nextjs.org/docs/app/building-your-application/data-fetching\n\n"), vec![span]
),
RSCErrorKind::NextRscErrDeprecatedApi((source, item, span)) => match (&*source, &*item) {
("next/server", "ImageResponse") => (
"ImageResponse moved from \"next/server\" to \"next/og\" since Next.js 14, please \
import from \"next/og\" instead"
.to_string(),
span,
vec![span],
),
_ => (format!("\"{source}\" is deprecated."), span),
_ => (format!("\"{source}\" is deprecated."), vec![span]),
},
RSCErrorKind::NextSsrDynamicFalseNotAllowed(span) => (
"`ssr: false` is not allowed with `next/dynamic` in Server Components. Please move it into a client component."
.to_string(),
span,
vec![span],
),
RSCErrorKind::NextRscErrIncompatibleDynamicIoSegment(span, segment) => (
format!("\"{}\" is not compatible with `nextConfig.experimental.dynamicIO`. Please remove it.", segment),
span,
format!("Route segment config \"{}\" is not compatible with `nextConfig.experimental.dynamicIO`. Please remove it.", segment),
vec![span],
),
};

HANDLER.with(|handler| handler.struct_span_err(span, msg.as_str()).emit())
HANDLER.with(|handler| handler.struct_span_err(spans, msg.as_str()).emit())
}

/// Collects top level directives and imports
Expand Down Expand Up @@ -752,30 +754,31 @@ impl ReactServerComponentValidator {
let is_layout_or_page = RE.is_match(&self.filepath);

if is_layout_or_page {
let mut span = DUMMY_SP;
let mut invalid_export_name = String::new();
let mut invalid_exports: HashMap<String, InvalidExportKind> = HashMap::new();

let mut invalid_exports_matcher = |export_name: &str| -> bool {
match export_name {
"getServerSideProps" | "getStaticProps" | "generateMetadata" | "metadata" => {
invalid_exports.insert(export_name.to_string(), InvalidExportKind::General);
true
let mut possibly_invalid_exports: IndexMap<String, (InvalidExportKind, Span)> =
IndexMap::new();

let mut collect_possibly_invalid_exports =
|export_name: &str, span: &Span| match export_name {
"getServerSideProps" | "getStaticProps" => {
possibly_invalid_exports
.insert(export_name.to_string(), (InvalidExportKind::General, *span));
}
"generateMetadata" | "metadata" => {
possibly_invalid_exports.insert(
export_name.to_string(),
(InvalidExportKind::Metadata, *span),
);
}
"dynamicParams" | "dynamic" | "fetchCache" | "runtime" | "revalidate" => {
if self.dynamic_io_enabled {
invalid_exports.insert(
possibly_invalid_exports.insert(
export_name.to_string(),
InvalidExportKind::DynamicIoSegment,
(InvalidExportKind::DynamicIoSegment, *span),
);
true
} else {
false
}
}
_ => false,
}
};
_ => (),
};

for export in &module.body {
match export {
Expand All @@ -784,35 +787,23 @@ impl ReactServerComponentValidator {
if let ExportSpecifier::Named(named) = specifier {
match &named.orig {
ModuleExportName::Ident(i) => {
if invalid_exports_matcher(&i.sym) {
span = named.span;
invalid_export_name = i.sym.to_string();
}
collect_possibly_invalid_exports(&i.sym, &named.span);
}
ModuleExportName::Str(s) => {
if invalid_exports_matcher(&s.value) {
span = named.span;
invalid_export_name = s.value.to_string();
}
collect_possibly_invalid_exports(&s.value, &named.span);
}
}
}
}
}
ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export)) => match &export.decl {
Decl::Fn(f) => {
if invalid_exports_matcher(&f.ident.sym) {
span = f.ident.span;
invalid_export_name = f.ident.sym.to_string();
}
collect_possibly_invalid_exports(&f.ident.sym, &f.ident.span);
}
Decl::Var(v) => {
for decl in &v.decls {
if let Pat::Ident(i) = &decl.name {
if invalid_exports_matcher(&i.sym) {
span = i.span;
invalid_export_name = i.sym.to_string();
}
collect_possibly_invalid_exports(&i.sym, &i.span);
}
}
}
Expand All @@ -822,59 +813,56 @@ impl ReactServerComponentValidator {
}
}

// Assert invalid metadata and generateMetadata exports.
let has_gm_export = invalid_exports.contains_key("generateMetadata");
let has_metadata_export = invalid_exports.contains_key("metadata");

for (export_name, kind) in &invalid_exports {
for (export_name, (kind, span)) in &possibly_invalid_exports {
match kind {
InvalidExportKind::DynamicIoSegment => {
report_error(
&self.app_dir,
&self.filepath,
RSCErrorKind::NextRscErrIncompatibleDynamicIoSegment(
span,
*span,
export_name.clone(),
),
);
}
InvalidExportKind::General => {
InvalidExportKind::Metadata => {
// Client entry can't export `generateMetadata` or `metadata`.
if is_client_entry {
if has_gm_export || has_metadata_export {
report_error(
&self.app_dir,
&self.filepath,
RSCErrorKind::NextRscErrClientMetadataExport((
invalid_export_name.clone(),
span,
)),
);
}
} else {
// Server entry can't export `generateMetadata` and `metadata` together.
if has_gm_export && has_metadata_export {
report_error(
&self.app_dir,
&self.filepath,
RSCErrorKind::NextRscErrConflictMetadataExport(span),
);
}
}
// Assert `getServerSideProps` and `getStaticProps` exports.
if invalid_export_name == "getServerSideProps"
|| invalid_export_name == "getStaticProps"
if is_client_entry
&& (export_name == "generateMetadata" || export_name == "metadata")
{
report_error(
&self.app_dir,
&self.filepath,
RSCErrorKind::NextRscErrInvalidApi((
invalid_export_name.clone(),
span,
RSCErrorKind::NextRscErrClientMetadataExport((
export_name.clone(),
*span,
)),
);
}
// Server entry can't export `generateMetadata` and `metadata` together,
// which is handled separately below.
}
InvalidExportKind::General => {
report_error(
&self.app_dir,
&self.filepath,
RSCErrorKind::NextRscErrInvalidApi((export_name.clone(), *span)),
);
}
}
}

// Server entry can't export `generateMetadata` and `metadata` together.
if !is_client_entry {
let export1 = possibly_invalid_exports.get("generateMetadata");
let export2 = possibly_invalid_exports.get("metadata");

if let (Some((_, span1)), Some((_, span2))) = (export1, export2) {
report_error(
&self.app_dir,
&self.filepath,
RSCErrorKind::NextRscErrConflictMetadataExport((*span1, *span2)),
);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
x You are attempting to export "getServerSideProps" from a component marked with "use client", which is disallowed. Either remove the export, or the "use client" directive. Read more: https://
x You are attempting to export "metadata" from a component marked with "use client", which is disallowed. Either remove the export, or the "use client" directive. Read more: https://nextjs.org/
| docs/app/api-reference/directives/use-client
|
|
,-[input.js:1:1]
1 | export const metadata = {}
: ^^^^^^^^
`----
x You are attempting to export "generateMetadata" from a component marked with "use client", which is disallowed. Either remove the export, or the "use client" directive. Read more: https://
| nextjs.org/docs/app/api-reference/directives/use-client
|
|
,-[input.js:5:1]
4 |
5 | export function getServerSideProps() {}
: ^^^^^^^^^^^^^^^^^^
,-[input.js:3:1]
2 |
3 | export function generateMetadata() {}
: ^^^^^^^^^^^^^^^^
`----
x "getServerSideProps" is not supported in app/. Read more: https://nextjs.org/docs/app/building-your-application/data-fetching
|
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,31 @@
x "dynamicParams" is not compatible with `nextConfig.experimental.dynamicIO`. Please remove it.
,-[input.js:5:1]
4 | export const fetchCache = 'force-no-store'
5 | export const revalidate = 1
: ^^^^^^^^^^
x Route segment config "runtime" is not compatible with `nextConfig.experimental.dynamicIO`. Please remove it.
,-[input.js:1:1]
1 | export const runtime = 'edge'
: ^^^^^^^
2 | export const dynamic = 'force-dynamic'
`----
x "dynamic" is not compatible with `nextConfig.experimental.dynamicIO`. Please remove it.
,-[input.js:5:1]
4 | export const fetchCache = 'force-no-store'
5 | export const revalidate = 1
: ^^^^^^^^^^
x Route segment config "dynamic" is not compatible with `nextConfig.experimental.dynamicIO`. Please remove it.
,-[input.js:2:1]
1 | export const runtime = 'edge'
2 | export const dynamic = 'force-dynamic'
: ^^^^^^^
3 | export const dynamicParams = false
`----
x "fetchCache" is not compatible with `nextConfig.experimental.dynamicIO`. Please remove it.
,-[input.js:5:1]
x Route segment config "dynamicParams" is not compatible with `nextConfig.experimental.dynamicIO`. Please remove it.
,-[input.js:3:1]
2 | export const dynamic = 'force-dynamic'
3 | export const dynamicParams = false
: ^^^^^^^^^^^^^
4 | export const fetchCache = 'force-no-store'
5 | export const revalidate = 1
: ^^^^^^^^^^
`----
x "runtime" is not compatible with `nextConfig.experimental.dynamicIO`. Please remove it.
,-[input.js:5:1]
x Route segment config "fetchCache" is not compatible with `nextConfig.experimental.dynamicIO`. Please remove it.
,-[input.js:4:1]
3 | export const dynamicParams = false
4 | export const fetchCache = 'force-no-store'
5 | export const revalidate = 1
: ^^^^^^^^^^
5 | export const revalidate = 1
`----
x "revalidate" is not compatible with `nextConfig.experimental.dynamicIO`. Please remove it.
x Route segment config "revalidate" is not compatible with `nextConfig.experimental.dynamicIO`. Please remove it.
,-[input.js:5:1]
4 | export const fetchCache = 'force-no-store'
5 | export const revalidate = 1
Expand Down
Loading

0 comments on commit bc3e597

Please sign in to comment.