mirror of
https://github.com/vercel/next.js.git
synced 2026-09-20 02:25:18 +08:00
bdbf63aef7
## Summary
**What?** Fixes the error reported for a module that has a top-level
`"use client"` directive and an inline `"use server"` function
directive, e.g. `<form action={async () => { "use server" }} />` in a
client page.
**Why?** Under Turbopack this reported a bogus error — `The "use client"
directive must be placed before other expressions` — even though the
directive was already at the top of the file. The actual mistake (an
inline server action inside a Client Component) was never surfaced.
**How?** On Turbopack's RSC layer the server actions transform runs
*before* the React Server Components assert. When it hoists an inline
action it prepended its generated imports (`registerServerReference`,
action encryption, cache runtime) at index 0 of the module — *above* the
`"use client"` directive, which the transform does not consume. The RSC
assert then saw an import before the directive and reported it as
misplaced.
Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
343 lines
12 KiB
Rust
343 lines
12 KiB
Rust
use std::{iter::FromIterator, path::PathBuf};
|
|
|
|
use next_custom_transforms::transforms::{
|
|
disallow_re_export_all_in_page::disallow_re_export_all_in_page,
|
|
dynamic::{NextDynamicMode, next_dynamic},
|
|
fonts::{Config as FontLoaderConfig, next_font_loaders},
|
|
next_ssg::next_ssg,
|
|
react_server_components::server_components,
|
|
server_actions::{self, ServerActionsMode, server_actions},
|
|
strip_page_exports::{ExportFilter, next_transform_strip_page_exports},
|
|
};
|
|
use rustc_hash::FxHashSet;
|
|
use swc_core::{
|
|
atoms::atom,
|
|
common::{FileName, Mark},
|
|
ecma::{
|
|
ast::fn_pass,
|
|
parser::{EsSyntax, Syntax},
|
|
transforms::{
|
|
base::resolver,
|
|
testing::{FixtureTestConfig, test_fixture},
|
|
},
|
|
visit::VisitWith,
|
|
},
|
|
};
|
|
use testing::fixture;
|
|
use turbo_rcstr::rcstr;
|
|
|
|
fn syntax() -> Syntax {
|
|
Syntax::Es(EsSyntax {
|
|
jsx: true,
|
|
..Default::default()
|
|
})
|
|
}
|
|
|
|
#[fixture("tests/errors/re-export-all-in-page/**/input.js")]
|
|
fn re_export_all_in_page(input: PathBuf) {
|
|
let output = input.parent().unwrap().join("output.js");
|
|
test_fixture(
|
|
syntax(),
|
|
&|_tr| disallow_re_export_all_in_page(true),
|
|
&input,
|
|
&output,
|
|
FixtureTestConfig {
|
|
allow_error: true,
|
|
module: Some(true),
|
|
..Default::default()
|
|
},
|
|
);
|
|
}
|
|
|
|
#[fixture("tests/errors/next-dynamic/**/input.js")]
|
|
fn next_dynamic_errors(input: PathBuf) {
|
|
let output = input.parent().unwrap().join("output.js");
|
|
test_fixture(
|
|
syntax(),
|
|
&|_tr| {
|
|
next_dynamic(
|
|
true,
|
|
false,
|
|
false,
|
|
false,
|
|
NextDynamicMode::Webpack,
|
|
FileName::Real(PathBuf::from("/some-project/src/some-file.js")).into(),
|
|
Some("/some-project/src".into()),
|
|
)
|
|
},
|
|
&input,
|
|
&output,
|
|
FixtureTestConfig {
|
|
allow_error: true,
|
|
module: Some(true),
|
|
..Default::default()
|
|
},
|
|
);
|
|
}
|
|
|
|
#[fixture("tests/errors/next-ssg/**/input.js")]
|
|
fn next_ssg_errors(input: PathBuf) {
|
|
let output = input.parent().unwrap().join("output.js");
|
|
test_fixture(
|
|
syntax(),
|
|
&|_tr| next_ssg(Default::default()),
|
|
&input,
|
|
&output,
|
|
FixtureTestConfig {
|
|
allow_error: true,
|
|
module: Some(true),
|
|
..Default::default()
|
|
},
|
|
);
|
|
}
|
|
|
|
#[fixture("tests/errors/react-server-components/**/input.js")]
|
|
#[fixture("tests/errors/react-server-components/**/page.js")]
|
|
#[fixture("tests/errors/react-server-components/**/route.js")]
|
|
#[fixture("tests/errors/react-server-components/**/sitemap.js")]
|
|
fn react_server_components_errors(input: PathBuf) {
|
|
use next_custom_transforms::transforms::react_server_components::{Config, Options};
|
|
let is_react_server_layer = input.iter().any(|s| s.to_str() == Some("server-graph"));
|
|
let cache_components_enabled = input.iter().any(|s| s.to_str() == Some("cache-components"));
|
|
let use_cache_enabled = input.iter().any(|s| s.to_str() == Some("use-cache"));
|
|
let taint_enabled = input.iter().any(|s| s.to_str() == Some("taint-enabled"));
|
|
|
|
// A path segment named `app-dir` marks the fixture as an App Router file.
|
|
// Everything up to and including that segment becomes `appDir`. A fixture
|
|
// without the segment compiles as a Pages Router file, so the checks that
|
|
// only apply inside `appDir` do not run for it.
|
|
let app_dir = input
|
|
.iter()
|
|
.position(|s| s.to_str() == Some("app-dir"))
|
|
.map(|pos| input.iter().take(pos + 1).collect());
|
|
|
|
let output = input.parent().unwrap().join("output.js");
|
|
test_fixture(
|
|
syntax(),
|
|
&|tr| {
|
|
server_components(
|
|
FileName::Real(input.clone()).into(),
|
|
Config::WithOptions(Options {
|
|
is_react_server_layer,
|
|
cache_components_enabled,
|
|
use_cache_enabled,
|
|
taint_enabled,
|
|
page_extensions: vec![],
|
|
}),
|
|
tr.comments.as_ref().clone(),
|
|
app_dir.clone(),
|
|
)
|
|
},
|
|
&input,
|
|
&output,
|
|
FixtureTestConfig {
|
|
allow_error: true,
|
|
module: Some(true),
|
|
..Default::default()
|
|
},
|
|
);
|
|
}
|
|
|
|
#[fixture("tests/errors/next-font-loaders/**/input.js")]
|
|
fn next_font_loaders_errors(input: PathBuf) {
|
|
let output = input.parent().unwrap().join("output.js");
|
|
test_fixture(
|
|
syntax(),
|
|
&|_tr| {
|
|
next_font_loaders(FontLoaderConfig {
|
|
relative_file_path_from_root: atom!("pages/test.tsx"),
|
|
font_loaders: vec![
|
|
atom!("@next/font/google").into(),
|
|
atom!("cool-fonts").into(),
|
|
],
|
|
})
|
|
},
|
|
&input,
|
|
&output,
|
|
FixtureTestConfig {
|
|
allow_error: true,
|
|
module: Some(true),
|
|
..Default::default()
|
|
},
|
|
);
|
|
}
|
|
|
|
#[fixture("tests/errors/server-actions/**/input.js")]
|
|
fn react_server_actions_errors(input: PathBuf) {
|
|
use next_custom_transforms::transforms::react_server_components::{Config, Options};
|
|
let is_react_server_layer = input.iter().any(|s| s.to_str() == Some("server-graph"));
|
|
let output = input.parent().unwrap().join("output.js");
|
|
test_fixture(
|
|
syntax(),
|
|
&|tr| {
|
|
let unresolved_mark = Mark::new();
|
|
(
|
|
// The transforms are intentionally declared in the same order as in
|
|
// crates/next-custom-transforms/src/chain_transforms.rs
|
|
resolver(unresolved_mark, Mark::new(), false),
|
|
server_components(
|
|
FileName::Real(PathBuf::from("/app/item.js")).into(),
|
|
Config::WithOptions(Options {
|
|
is_react_server_layer,
|
|
cache_components_enabled: true,
|
|
use_cache_enabled: true,
|
|
taint_enabled: true,
|
|
page_extensions: vec![],
|
|
}),
|
|
tr.comments.as_ref().clone(),
|
|
None,
|
|
),
|
|
server_actions(
|
|
&FileName::Real("/app/item.js".into()),
|
|
None,
|
|
server_actions::Config {
|
|
is_react_server_layer,
|
|
is_development: true,
|
|
use_cache_enabled: true,
|
|
hash_salt: "".into(),
|
|
cache_kinds: FxHashSet::default(),
|
|
},
|
|
tr.comments.as_ref().clone(),
|
|
unresolved_mark,
|
|
tr.cm.clone(),
|
|
Default::default(),
|
|
ServerActionsMode::Webpack,
|
|
),
|
|
)
|
|
},
|
|
&input,
|
|
&output,
|
|
FixtureTestConfig {
|
|
allow_error: true,
|
|
module: Some(true),
|
|
..Default::default()
|
|
},
|
|
);
|
|
}
|
|
|
|
// Turbopack runs the server actions transform before the React Server Components assert on
|
|
// the RSC layer (unlike webpack, which runs the RSC transform first). See
|
|
// `crates/next-core/src/next_server/context.rs` (AppRSC module rules).
|
|
#[fixture("tests/errors/server-actions-turbopack-rsc/**/input.js")]
|
|
fn server_actions_turbopack_rsc_errors(input: PathBuf) {
|
|
use next_custom_transforms::transforms::react_server_components::{
|
|
Config, Options, server_components_assert,
|
|
};
|
|
let output = input.parent().unwrap().join("output.js");
|
|
test_fixture(
|
|
syntax(),
|
|
&|tr| {
|
|
let unresolved_mark = Mark::new();
|
|
(
|
|
resolver(unresolved_mark, Mark::new(), false),
|
|
server_actions(
|
|
&FileName::Real("/app/item.js".into()),
|
|
None,
|
|
server_actions::Config {
|
|
is_react_server_layer: true,
|
|
is_development: true,
|
|
use_cache_enabled: true,
|
|
hash_salt: "".into(),
|
|
cache_kinds: FxHashSet::default(),
|
|
},
|
|
tr.comments.as_ref().clone(),
|
|
unresolved_mark,
|
|
tr.cm.clone(),
|
|
Default::default(),
|
|
ServerActionsMode::Turbopack,
|
|
),
|
|
fn_pass(|program| {
|
|
program.visit_with(&mut server_components_assert(
|
|
FileName::Real(PathBuf::from("/app/item.js")),
|
|
Config::WithOptions(Options {
|
|
is_react_server_layer: true,
|
|
cache_components_enabled: true,
|
|
use_cache_enabled: true,
|
|
taint_enabled: true,
|
|
page_extensions: vec![],
|
|
}),
|
|
None,
|
|
));
|
|
}),
|
|
)
|
|
},
|
|
&input,
|
|
&output,
|
|
FixtureTestConfig {
|
|
allow_error: true,
|
|
module: Some(true),
|
|
..Default::default()
|
|
},
|
|
);
|
|
}
|
|
|
|
#[fixture("tests/errors/strip-page-exports/**/input.js")]
|
|
fn next_transform_strip_page_exports_errors(input: PathBuf) {
|
|
let output = input.parent().unwrap().join("output.js");
|
|
test_fixture(
|
|
syntax(),
|
|
&|_tr| {
|
|
next_transform_strip_page_exports(ExportFilter::StripDataExports, Default::default())
|
|
},
|
|
&input,
|
|
&output,
|
|
FixtureTestConfig {
|
|
allow_error: true,
|
|
module: Some(true),
|
|
..Default::default()
|
|
},
|
|
);
|
|
}
|
|
|
|
#[fixture("tests/errors/use-cache-not-allowed/**/input.js")]
|
|
fn use_cache_not_allowed(input: PathBuf) {
|
|
use next_custom_transforms::transforms::react_server_components::{Config, Options};
|
|
let output = input.parent().unwrap().join("output.js");
|
|
test_fixture(
|
|
syntax(),
|
|
&|tr| {
|
|
let unresolved_mark = Mark::new();
|
|
(
|
|
// The transforms are intentionally declared in the same order as in
|
|
// crates/next-custom-transforms/src/chain_transforms.rs
|
|
resolver(unresolved_mark, Mark::new(), false),
|
|
server_components(
|
|
FileName::Real(PathBuf::from("/app/item.js")).into(),
|
|
Config::WithOptions(Options {
|
|
is_react_server_layer: true,
|
|
cache_components_enabled: false,
|
|
use_cache_enabled: false,
|
|
taint_enabled: true,
|
|
page_extensions: vec![],
|
|
}),
|
|
tr.comments.as_ref().clone(),
|
|
None,
|
|
),
|
|
server_actions(
|
|
&FileName::Real("/app/item.js".into()),
|
|
None,
|
|
server_actions::Config {
|
|
is_react_server_layer: true,
|
|
is_development: true,
|
|
use_cache_enabled: false,
|
|
hash_salt: "".into(),
|
|
cache_kinds: FxHashSet::from_iter([rcstr!("x")]),
|
|
},
|
|
tr.comments.as_ref().clone(),
|
|
unresolved_mark,
|
|
tr.cm.clone(),
|
|
Default::default(),
|
|
ServerActionsMode::Webpack,
|
|
),
|
|
)
|
|
},
|
|
&input,
|
|
&output,
|
|
FixtureTestConfig {
|
|
allow_error: true,
|
|
module: Some(true),
|
|
..Default::default()
|
|
},
|
|
);
|
|
}
|