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

Fix export statement validations for app router pages #75278

Merged
merged 3 commits into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading