From d5fa555841eb16904e1c8dee73d6f4b2ab4c1833 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Sat, 17 Sep 2022 00:12:59 +0200 Subject: [PATCH] Implement SWC transformer for server and client graphs (#40603) This is an initial implementation of the Server Components SWC transformer. For the server graph, it detects client entries via the `"client"` directive and transpile them into module reference code; for the client graph, it removes the directives. And for both graphs, it checks if there is any invalid imports for the given environment and shows proper errors. With that added, we can switch from `next-flight-client-loader` to directly use the SWC loader in one pass. Next step is to get rid of the `.client.` extension in other plugins. ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have helpful link attached, see `contributing.md` ## Feature - [x] 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` - [x] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have 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.md#adding-examples) --- packages/next-swc/crates/core/src/lib.rs | 18 +- .../core/src/react_server_components.rs | 341 ++++++++++++++++++ packages/next-swc/crates/core/tests/errors.rs | 40 +- .../client-graph/client-only/input.js | 13 + .../client-graph/client-only/output.js | 8 + .../client-graph/server-only/input.js | 13 + .../client-graph/server-only/output.js | 8 + .../client-graph/server-only/output.stderr | 6 + .../server-graph/client-only/input.js | 13 + .../server-graph/client-only/output.js | 8 + .../server-graph/client-only/output.stderr | 6 + .../server-graph/react-api/input.js | 21 ++ .../server-graph/react-api/output.js | 7 + .../server-graph/react-api/output.stderr | 78 ++++ .../server-graph/react-dom-api/input.js | 9 + .../server-graph/react-dom-api/output.js | 4 + .../server-graph/react-dom-api/output.stderr | 18 + .../react-dom-server-client/input.js | 15 + .../react-dom-server-client/output.js | 9 + .../react-dom-server-client/output.stderr | 12 + .../next-swc/crates/core/tests/fixture.rs | 39 ++ .../client-graph/client-entry/input.js | 31 ++ .../client-graph/client-entry/output.js | 13 + .../server-graph/client-entry/input.js | 27 ++ .../server-graph/client-entry/output.js | 3 + packages/next-swc/crates/core/tests/full.rs | 1 + packages/next/build/swc/options.js | 8 + packages/next/build/webpack-config.ts | 36 +- .../next-flight-client-loader/index.ts | 2 +- .../build/webpack/loaders/next-swc-loader.js | 4 +- packages/next/build/webpack/loaders/utils.ts | 21 +- .../client/components/app-router.client.tsx | 2 + .../client/components/hot-reloader.client.tsx | 2 + .../components/layout-router.client.tsx | 2 + .../render-from-template-context.client.tsx | 2 + packages/next/client/future/image.tsx | 2 + packages/next/client/image.tsx | 2 + packages/next/client/link.tsx | 2 + packages/next/client/script.tsx | 2 + packages/next/lib/constants.ts | 1 + packages/next/shared/lib/dynamic.tsx | 2 + packages/next/shared/lib/head.tsx | 2 + packages/next/taskfile-swc.js | 9 +- packages/next/taskfile.js | 1 + .../app/client-component-route/page.client.js | 2 + .../app/app/client-nested/layout.client.js | 2 + .../get-server-side-props/page.client.js | 2 + .../get-static-props/page.client.js | 2 + .../app/app/css/css-client/layout.client.js | 2 + .../app/app/css/css-client/page.client.js | 2 + .../app/app/css/css-nested/layout.client.js | 2 + .../app/app/css/css-nested/page.client.js | 2 + .../app/app/dashboard/client-comp.client.jsx | 2 + .../index/dynamic-imports/dynamic.client.js | 2 + .../dynamic-imports/react-lazy.client.js | 2 + .../dashboard/index/text-dynamic.client.js | 2 + .../app/dashboard/index/text-lazy.client.js | 2 + .../app/error/clientcomponent/error.client.js | 2 + .../app/error/clientcomponent/page.client.js | 2 + .../ssr-error-client-component/page.client.js | 2 + .../hooks/use-cookies/client/page.client.js | 2 + .../hooks/use-headers/client/page.client.js | 2 + .../app/app/hooks/use-pathname/page.client.js | 2 + .../use-preview-data/client/page.client.js | 2 + .../app/app/hooks/use-router/page.client.js | 2 + .../hooks/use-router/sub-page/page.client.js | 2 + .../hooks/use-search-params/page.client.js | 2 + .../app-dir/app/app/navigation/link.client.js | 2 + .../app/app/old-router/Router.client.js | 2 + .../app/param-and-query/[slug]/page.client.js | 2 + .../should-not-serve-client/page.client.js | 2 + .../clientcomponent/template.client.js | 2 + test/e2e/app-dir/index.test.ts | 4 +- .../app/css-in-js/styled-components.client.js | 2 + .../app/css-in-js/styled-jsx.client.js | 2 + .../app/external-imports/page.client.js | 2 + .../app/root-style-registry.client.js | 2 + .../rsc-basic/components/bar.client.js | 2 + .../rsc-basic/components/cjs.client.js | 2 + .../components/client-exports.client.js | 2 + .../rsc-basic/components/client.client.js | 2 + .../components/export-all/index.client.js | 2 + .../rsc-basic/components/foo.client.js | 2 + .../partial-hydration-counter.client.js | 2 + .../random-module-instance.client.js | 2 + .../rsc-basic/components/red/index.client.js | 2 + .../components/router-path.client.js | 2 + .../rsc-basic/components/shared.client.js | 2 + .../app-dir/rsc-basic/components/shared.js | 4 +- 89 files changed, 924 insertions(+), 33 deletions(-) create mode 100644 packages/next-swc/crates/core/src/react_server_components.rs create mode 100644 packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/client-only/input.js create mode 100644 packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/client-only/output.js create mode 100644 packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/server-only/input.js create mode 100644 packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/server-only/output.js create mode 100644 packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/server-only/output.stderr create mode 100644 packages/next-swc/crates/core/tests/errors/react-server-components/server-graph/client-only/input.js create mode 100644 packages/next-swc/crates/core/tests/errors/react-server-components/server-graph/client-only/output.js create mode 100644 packages/next-swc/crates/core/tests/errors/react-server-components/server-graph/client-only/output.stderr create mode 100644 packages/next-swc/crates/core/tests/errors/react-server-components/server-graph/react-api/input.js create mode 100644 packages/next-swc/crates/core/tests/errors/react-server-components/server-graph/react-api/output.js create mode 100644 packages/next-swc/crates/core/tests/errors/react-server-components/server-graph/react-api/output.stderr create mode 100644 packages/next-swc/crates/core/tests/errors/react-server-components/server-graph/react-dom-api/input.js create mode 100644 packages/next-swc/crates/core/tests/errors/react-server-components/server-graph/react-dom-api/output.js create mode 100644 packages/next-swc/crates/core/tests/errors/react-server-components/server-graph/react-dom-api/output.stderr create mode 100644 packages/next-swc/crates/core/tests/errors/react-server-components/server-graph/react-dom-server-client/input.js create mode 100644 packages/next-swc/crates/core/tests/errors/react-server-components/server-graph/react-dom-server-client/output.js create mode 100644 packages/next-swc/crates/core/tests/errors/react-server-components/server-graph/react-dom-server-client/output.stderr create mode 100644 packages/next-swc/crates/core/tests/fixture/react-server-components/client-graph/client-entry/input.js create mode 100644 packages/next-swc/crates/core/tests/fixture/react-server-components/client-graph/client-entry/output.js create mode 100644 packages/next-swc/crates/core/tests/fixture/react-server-components/server-graph/client-entry/input.js create mode 100644 packages/next-swc/crates/core/tests/fixture/react-server-components/server-graph/client-entry/output.js diff --git a/packages/next-swc/crates/core/src/lib.rs b/packages/next-swc/crates/core/src/lib.rs index 68e1efe0d074..4ebe788b24e1 100644 --- a/packages/next-swc/crates/core/src/lib.rs +++ b/packages/next-swc/crates/core/src/lib.rs @@ -54,6 +54,7 @@ pub mod next_dynamic; pub mod next_ssg; pub mod page_config; pub mod react_remove_properties; +pub mod react_server_components; #[cfg(not(target_arch = "wasm32"))] pub mod relay; pub mod remove_console; @@ -84,6 +85,9 @@ pub struct TransformOptions { #[serde(default)] pub is_server: bool, + #[serde(default)] + pub server_components: Option, + #[serde(default)] pub styled_components: Option, @@ -113,7 +117,10 @@ pub fn custom_before_pass<'a, C: Comments + 'a>( opts: &'a TransformOptions, comments: C, eliminated_packages: Rc>>, -) -> impl Fold + 'a { +) -> impl Fold + 'a +where + C: Clone, +{ #[cfg(target_arch = "wasm32")] let relay_plugin = noop(); @@ -132,6 +139,15 @@ pub fn custom_before_pass<'a, C: Comments + 'a>( chain!( disallow_re_export_all_in_page::disallow_re_export_all_in_page(opts.is_page_file), + match &opts.server_components { + Some(config) if config.truthy() => + Either::Left(react_server_components::server_components( + file.name.clone(), + config.clone(), + comments.clone(), + )), + _ => Either::Right(noop()), + }, styled_jsx::styled_jsx(cm.clone(), file.name.clone()), hook_optimizer::hook_optimizer(), match &opts.styled_components { diff --git a/packages/next-swc/crates/core/src/react_server_components.rs b/packages/next-swc/crates/core/src/react_server_components.rs new file mode 100644 index 000000000000..b172ea47fc4e --- /dev/null +++ b/packages/next-swc/crates/core/src/react_server_components.rs @@ -0,0 +1,341 @@ +use serde::Deserialize; + +use swc_core::{ + common::{ + comments::{Comment, CommentKind, Comments}, + errors::HANDLER, + FileName, Span, DUMMY_SP, + }, + ecma::ast::*, + ecma::atoms::{js_word, JsWord}, + ecma::utils::{prepend_stmts, quote_ident, quote_str, ExprFactory}, + ecma::visit::{as_folder, noop_visit_mut_type, Fold, VisitMut, VisitMutWith}, +}; + +#[derive(Clone, Debug, Deserialize)] +#[serde(untagged)] +pub enum Config { + All(bool), + WithOptions(Options), +} + +impl Config { + pub fn truthy(&self) -> bool { + match self { + Config::All(b) => *b, + Config::WithOptions(_) => true, + } + } +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Options { + pub is_server: bool, +} + +struct ReactServerComponents { + is_server: bool, + filepath: String, + comments: C, + invalid_server_imports: Vec, + invalid_client_imports: Vec, + invalid_server_react_apis: Vec, + invalid_server_react_dom_apis: Vec, +} + +struct ModuleImports { + source: (JsWord, Span), + specifiers: Vec<(JsWord, Span)>, +} + +impl VisitMut for ReactServerComponents { + noop_visit_mut_type!(); + + fn visit_mut_module(&mut self, module: &mut Module) { + let (is_client_entry, imports) = self.collect_top_level_directives_and_imports(module); + + if self.is_server { + if !is_client_entry { + self.assert_server_graph(&imports); + } else { + self.to_module_ref(module); + return; + } + } else { + self.assert_client_graph(&imports); + } + module.visit_mut_children_with(self) + } +} + +impl ReactServerComponents { + // Collects top level directives and imports, then removes specific ones + // from the AST. + fn collect_top_level_directives_and_imports( + &self, + module: &mut Module, + ) -> (bool, Vec) { + let mut imports: Vec = vec![]; + let mut finished_directives = false; + let mut is_client_entry = false; + + let _ = &module.body.retain(|item| { + match item { + ModuleItem::Stmt(stmt) => { + if !finished_directives { + if !stmt.is_expr() { + // Not an expression. + finished_directives = true; + } + + match stmt.as_expr() { + Some(expr_stmt) => { + match &*expr_stmt.expr { + Expr::Lit(Lit::Str(Str { value, .. })) => { + if &**value == "client" { + is_client_entry = true; + + // Remove the directive. + return false; + } + } + _ => { + // Other expression types. + finished_directives = true; + } + } + } + None => { + // Not an expression. + finished_directives = true; + } + } + } + } + ModuleItem::ModuleDecl(ModuleDecl::Import(import)) => { + let source = import.src.value.clone(); + let specifiers = import + .specifiers + .iter() + .map(|specifier| match specifier { + ImportSpecifier::Named(named) => match &named.imported { + Some(imported) => match &imported { + ModuleExportName::Ident(i) => (i.to_id().0, i.span), + ModuleExportName::Str(s) => (s.value.clone(), s.span), + }, + None => (named.local.to_id().0, named.local.span), + }, + ImportSpecifier::Default(d) => (js_word!(""), d.span), + ImportSpecifier::Namespace(n) => ("*".into(), n.span), + }) + .collect(); + + imports.push(ModuleImports { + source: (source, import.span), + specifiers, + }); + + finished_directives = true; + } + _ => { + finished_directives = true; + } + } + true + }); + + (is_client_entry, imports) + } + + // Convert the client module to the module reference code and add a special + // comment to the top of the file. + fn to_module_ref(&self, module: &mut Module) { + // Clear all the statements and module declarations. + module.body.clear(); + + let proxy_ident = quote_ident!("createProxy"); + let filepath = quote_str!(&*self.filepath); + + prepend_stmts( + &mut module.body, + vec![ + ModuleItem::Stmt(Stmt::Decl(Decl::Var(VarDecl { + span: DUMMY_SP, + kind: VarDeclKind::Const, + decls: vec![VarDeclarator { + span: DUMMY_SP, + name: Pat::Object(ObjectPat { + span: DUMMY_SP, + props: vec![ObjectPatProp::Assign(AssignPatProp { + span: DUMMY_SP, + key: proxy_ident, + value: None, + })], + optional: false, + type_ann: None, + }), + init: Some(Box::new(Expr::Call(CallExpr { + span: DUMMY_SP, + callee: quote_ident!("require").as_callee(), + args: vec![quote_str!("private-next-rsc-mod-ref-proxy").as_arg()], + type_args: Default::default(), + }))), + definite: false, + }], + declare: false, + }))), + ModuleItem::Stmt(Stmt::Expr(ExprStmt { + span: DUMMY_SP, + expr: Box::new(Expr::Assign(AssignExpr { + span: DUMMY_SP, + left: PatOrExpr::Expr(Box::new(Expr::Member(MemberExpr { + span: DUMMY_SP, + obj: Box::new(Expr::Ident(quote_ident!("module"))), + prop: MemberProp::Ident(quote_ident!("exports")), + }))), + op: op!("="), + right: Box::new(Expr::Call(CallExpr { + span: DUMMY_SP, + callee: quote_ident!("createProxy").as_callee(), + args: vec![filepath.as_arg()], + type_args: Default::default(), + })), + })), + })), + ] + .into_iter(), + ); + + // Prepend a special comment to the top of the file. + self.comments.add_leading( + module.span.lo, + Comment { + span: DUMMY_SP, + kind: CommentKind::Block, + text: " __next_internal_client_entry_do_not_use__ ".into(), + }, + ); + } + + fn assert_server_graph(&self, imports: &Vec) { + for import in imports { + let source = import.source.0.clone(); + if self.invalid_server_imports.contains(&source) { + HANDLER.with(|handler| { + handler + .struct_span_err( + import.source.1, + format!( + "Disallowed import of `{}` in the Server Components compilation.", + source + ) + .as_str(), + ) + .emit() + }) + } + if source == *"react" { + for specifier in &import.specifiers { + if self.invalid_server_react_apis.contains(&specifier.0) { + HANDLER.with(|handler| { + handler + .struct_span_err( + specifier.1, + format!( + "Disallowed React API `{}` in the Server Components \ + compilation.", + &specifier.0 + ) + .as_str(), + ) + .emit() + }) + } + } + } + if source == *"react-dom" { + for specifier in &import.specifiers { + if self.invalid_server_react_dom_apis.contains(&specifier.0) { + HANDLER.with(|handler| { + handler + .struct_span_err( + specifier.1, + format!( + "Disallowed ReactDOM API `{}` in the Server Components \ + compilation.", + &specifier.0 + ) + .as_str(), + ) + .emit() + }) + } + } + } + } + } + + fn assert_client_graph(&self, imports: &Vec) { + for import in imports { + let source = import.source.0.clone(); + if self.invalid_client_imports.contains(&source) { + HANDLER.with(|handler| { + handler + .struct_span_err( + import.source.1, + format!( + "Disallowed import of `{}` in the Client Components compilation.", + source + ) + .as_str(), + ) + .emit() + }) + } + } + } +} + +pub fn server_components( + filename: FileName, + config: Config, + comments: C, +) -> impl Fold + VisitMut { + let is_server: bool = match config { + Config::WithOptions(x) => x.is_server, + _ => true, + }; + as_folder(ReactServerComponents { + is_server, + comments, + filepath: filename.to_string(), + invalid_server_imports: vec![ + JsWord::from("client-only"), + JsWord::from("react-dom/client"), + JsWord::from("react-dom/server"), + ], + invalid_client_imports: vec![JsWord::from("server-only")], + invalid_server_react_dom_apis: vec![ + JsWord::from("findDOMNode"), + JsWord::from("flushSync"), + JsWord::from("unstable_batchedUpdates"), + ], + invalid_server_react_apis: vec![ + JsWord::from("Component"), + JsWord::from("createContext"), + JsWord::from("createFactory"), + JsWord::from("PureComponent"), + JsWord::from("useDeferredValue"), + JsWord::from("useEffect"), + JsWord::from("useImperativeHandle"), + JsWord::from("useInsertionEffect"), + JsWord::from("useLayoutEffect"), + JsWord::from("useReducer"), + JsWord::from("useRef"), + JsWord::from("useState"), + JsWord::from("useSyncExternalStore"), + JsWord::from("useTransition"), + ], + }) +} diff --git a/packages/next-swc/crates/core/tests/errors.rs b/packages/next-swc/crates/core/tests/errors.rs index 6af191fe96da..3b6996807e5d 100644 --- a/packages/next-swc/crates/core/tests/errors.rs +++ b/packages/next-swc/crates/core/tests/errors.rs @@ -1,6 +1,6 @@ use next_swc::{ disallow_re_export_all_in_page::disallow_re_export_all_in_page, next_dynamic::next_dynamic, - next_ssg::next_ssg, + next_ssg::next_ssg, react_server_components::server_components, }; use std::path::PathBuf; use swc_core::{ @@ -56,3 +56,41 @@ fn next_ssg_errors(input: PathBuf) { &output, ); } + +#[fixture("tests/errors/react-server-components/server-graph/**/input.js")] +fn react_server_components_server_graph_errors(input: PathBuf) { + let output = input.parent().unwrap().join("output.js"); + test_fixture_allowing_error( + syntax(), + &|tr| { + server_components( + FileName::Real(PathBuf::from("/some-project/src/some-file.js")), + next_swc::react_server_components::Config::WithOptions( + next_swc::react_server_components::Options { is_server: true }, + ), + tr.comments.as_ref().clone(), + ) + }, + &input, + &output, + ); +} + +#[fixture("tests/errors/react-server-components/client-graph/**/input.js")] +fn react_server_components_client_graph_errors(input: PathBuf) { + let output = input.parent().unwrap().join("output.js"); + test_fixture_allowing_error( + syntax(), + &|tr| { + server_components( + FileName::Real(PathBuf::from("/some-project/src/some-file.js")), + next_swc::react_server_components::Config::WithOptions( + next_swc::react_server_components::Options { is_server: false }, + ), + tr.comments.as_ref().clone(), + ) + }, + &input, + &output, + ); +} diff --git a/packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/client-only/input.js b/packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/client-only/input.js new file mode 100644 index 000000000000..ec63d654d910 --- /dev/null +++ b/packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/client-only/input.js @@ -0,0 +1,13 @@ +// This is a comment. + +"use strict"; + +/** + * This is a comment. + */ + +import "client-only" + +export default function () { + return null; +} diff --git a/packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/client-only/output.js b/packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/client-only/output.js new file mode 100644 index 000000000000..b7e34fed0ea2 --- /dev/null +++ b/packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/client-only/output.js @@ -0,0 +1,8 @@ +// This is a comment. +"use strict"; +/** + * This is a comment. + */ import "client-only"; +export default function() { + return null; +} diff --git a/packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/server-only/input.js b/packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/server-only/input.js new file mode 100644 index 000000000000..02b271d25120 --- /dev/null +++ b/packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/server-only/input.js @@ -0,0 +1,13 @@ +// This is a comment. + +"use strict"; + +/** + * This is a comment. + */ + +import "server-only" + +export default function () { + return null; +} diff --git a/packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/server-only/output.js b/packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/server-only/output.js new file mode 100644 index 000000000000..889d68cc97f2 --- /dev/null +++ b/packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/server-only/output.js @@ -0,0 +1,8 @@ +// This is a comment. +"use strict"; +/** + * This is a comment. + */ import "server-only"; +export default function() { + return null; +} diff --git a/packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/server-only/output.stderr b/packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/server-only/output.stderr new file mode 100644 index 000000000000..30b0a47ff472 --- /dev/null +++ b/packages/next-swc/crates/core/tests/errors/react-server-components/client-graph/server-only/output.stderr @@ -0,0 +1,6 @@ + + x Disallowed import of `server-only` in the Client Components compilation. + ,-[input.js:9:1] + 9 | import "server-only" + : ^^^^^^^^^^^^^^^^^^^^ + `---- diff --git a/packages/next-swc/crates/core/tests/errors/react-server-components/server-graph/client-only/input.js b/packages/next-swc/crates/core/tests/errors/react-server-components/server-graph/client-only/input.js new file mode 100644 index 000000000000..ec63d654d910 --- /dev/null +++ b/packages/next-swc/crates/core/tests/errors/react-server-components/server-graph/client-only/input.js @@ -0,0 +1,13 @@ +// This is a comment. + +"use strict"; + +/** + * This is a comment. + */ + +import "client-only" + +export default function () { + return null; +} diff --git a/packages/next-swc/crates/core/tests/errors/react-server-components/server-graph/client-only/output.js b/packages/next-swc/crates/core/tests/errors/react-server-components/server-graph/client-only/output.js new file mode 100644 index 000000000000..b7e34fed0ea2 --- /dev/null +++ b/packages/next-swc/crates/core/tests/errors/react-server-components/server-graph/client-only/output.js @@ -0,0 +1,8 @@ +// This is a comment. +"use strict"; +/** + * This is a comment. + */ import "client-only"; +export default function() { + return null; +} diff --git a/packages/next-swc/crates/core/tests/errors/react-server-components/server-graph/client-only/output.stderr b/packages/next-swc/crates/core/tests/errors/react-server-components/server-graph/client-only/output.stderr new file mode 100644 index 000000000000..f3d8e080827d --- /dev/null +++ b/packages/next-swc/crates/core/tests/errors/react-server-components/server-graph/client-only/output.stderr @@ -0,0 +1,6 @@ + + x Disallowed import of `client-only` in the Server Components compilation. + ,-[input.js:9:1] + 9 | import "client-only" + : ^^^^^^^^^^^^^^^^^^^^ + `---- diff --git a/packages/next-swc/crates/core/tests/errors/react-server-components/server-graph/react-api/input.js b/packages/next-swc/crates/core/tests/errors/react-server-components/server-graph/react-api/input.js new file mode 100644 index 000000000000..a9d6953d0b4c --- /dev/null +++ b/packages/next-swc/crates/core/tests/errors/react-server-components/server-graph/react-api/input.js @@ -0,0 +1,21 @@ +import { useState } from 'react' + +import { createContext } from 'react' + +import { useEffect, useImperativeHandle } from 'react' + +import { + Component, + createFactory, + PureComponent, + useDeferredValue, + useInsertionEffect, + useLayoutEffect, + useReducer, + useRef, + useSyncExternalStore +} from "react" + +export default function () { + return null; +} diff --git a/packages/next-swc/crates/core/tests/errors/react-server-components/server-graph/react-api/output.js b/packages/next-swc/crates/core/tests/errors/react-server-components/server-graph/react-api/output.js new file mode 100644 index 000000000000..39c2869473c3 --- /dev/null +++ b/packages/next-swc/crates/core/tests/errors/react-server-components/server-graph/react-api/output.js @@ -0,0 +1,7 @@ +import { useState } from 'react'; +import { createContext } from 'react'; +import { useEffect, useImperativeHandle } from 'react'; +import { Component, createFactory, PureComponent, useDeferredValue, useInsertionEffect, useLayoutEffect, useReducer, useRef, useSyncExternalStore } from "react"; +export default function() { + return null; +} diff --git a/packages/next-swc/crates/core/tests/errors/react-server-components/server-graph/react-api/output.stderr b/packages/next-swc/crates/core/tests/errors/react-server-components/server-graph/react-api/output.stderr new file mode 100644 index 000000000000..dde1083903f3 --- /dev/null +++ b/packages/next-swc/crates/core/tests/errors/react-server-components/server-graph/react-api/output.stderr @@ -0,0 +1,78 @@ + + x Disallowed React API `useState` in the Server Components compilation. + ,-[input.js:1:1] + 1 | import { useState } from 'react' + : ^^^^^^^^ + `---- + + x Disallowed React API `createContext` in the Server Components compilation. + ,-[input.js:3:1] + 3 | import { createContext } from 'react' + : ^^^^^^^^^^^^^ + `---- + + x Disallowed React API `useEffect` in the Server Components compilation. + ,-[input.js:5:1] + 5 | import { useEffect, useImperativeHandle } from 'react' + : ^^^^^^^^^ + `---- + + x Disallowed React API `useImperativeHandle` in the Server Components compilation. + ,-[input.js:5:1] + 5 | import { useEffect, useImperativeHandle } from 'react' + : ^^^^^^^^^^^^^^^^^^^ + `---- + + x Disallowed React API `Component` in the Server Components compilation. + ,-[input.js:8:5] + 8 | Component, + : ^^^^^^^^^ + `---- + + x Disallowed React API `createFactory` in the Server Components compilation. + ,-[input.js:9:5] + 9 | createFactory, + : ^^^^^^^^^^^^^ + `---- + + x Disallowed React API `PureComponent` in the Server Components compilation. + ,-[input.js:10:5] + 10 | PureComponent, + : ^^^^^^^^^^^^^ + `---- + + x Disallowed React API `useDeferredValue` in the Server Components compilation. + ,-[input.js:11:3] + 11 | useDeferredValue, + : ^^^^^^^^^^^^^^^^ + `---- + + x Disallowed React API `useInsertionEffect` in the Server Components compilation. + ,-[input.js:12:5] + 12 | useInsertionEffect, + : ^^^^^^^^^^^^^^^^^^ + `---- + + x Disallowed React API `useLayoutEffect` in the Server Components compilation. + ,-[input.js:13:5] + 13 | useLayoutEffect, + : ^^^^^^^^^^^^^^^ + `---- + + x Disallowed React API `useReducer` in the Server Components compilation. + ,-[input.js:14:5] + 14 | useReducer, + : ^^^^^^^^^^ + `---- + + x Disallowed React API `useRef` in the Server Components compilation. + ,-[input.js:15:5] + 15 | useRef, + : ^^^^^^ + `---- + + x Disallowed React API `useSyncExternalStore` in the Server Components compilation. + ,-[input.js:16:5] + 16 | useSyncExternalStore + : ^^^^^^^^^^^^^^^^^^^^ + `---- diff --git a/packages/next-swc/crates/core/tests/errors/react-server-components/server-graph/react-dom-api/input.js b/packages/next-swc/crates/core/tests/errors/react-server-components/server-graph/react-dom-api/input.js new file mode 100644 index 000000000000..d85a1b446333 --- /dev/null +++ b/packages/next-swc/crates/core/tests/errors/react-server-components/server-graph/react-dom-api/input.js @@ -0,0 +1,9 @@ +import { + findDOMNode, + flushSync, + unstable_batchedUpdates, +} from "react-dom" + +export default function () { + return null; +} diff --git a/packages/next-swc/crates/core/tests/errors/react-server-components/server-graph/react-dom-api/output.js b/packages/next-swc/crates/core/tests/errors/react-server-components/server-graph/react-dom-api/output.js new file mode 100644 index 000000000000..ca0fe9d0d95e --- /dev/null +++ b/packages/next-swc/crates/core/tests/errors/react-server-components/server-graph/react-dom-api/output.js @@ -0,0 +1,4 @@ +import { findDOMNode, flushSync, unstable_batchedUpdates } from "react-dom"; +export default function() { + return null; +} diff --git a/packages/next-swc/crates/core/tests/errors/react-server-components/server-graph/react-dom-api/output.stderr b/packages/next-swc/crates/core/tests/errors/react-server-components/server-graph/react-dom-api/output.stderr new file mode 100644 index 000000000000..1e943500e0dd --- /dev/null +++ b/packages/next-swc/crates/core/tests/errors/react-server-components/server-graph/react-dom-api/output.stderr @@ -0,0 +1,18 @@ + + x Disallowed ReactDOM API `findDOMNode` in the Server Components compilation. + ,-[input.js:2:5] + 2 | findDOMNode, + : ^^^^^^^^^^^ + `---- + + x Disallowed ReactDOM API `flushSync` in the Server Components compilation. + ,-[input.js:3:3] + 3 | flushSync, + : ^^^^^^^^^ + `---- + + x Disallowed ReactDOM API `unstable_batchedUpdates` in the Server Components compilation. + ,-[input.js:4:3] + 4 | unstable_batchedUpdates, + : ^^^^^^^^^^^^^^^^^^^^^^^ + `---- diff --git a/packages/next-swc/crates/core/tests/errors/react-server-components/server-graph/react-dom-server-client/input.js b/packages/next-swc/crates/core/tests/errors/react-server-components/server-graph/react-dom-server-client/input.js new file mode 100644 index 000000000000..f39cce47429d --- /dev/null +++ b/packages/next-swc/crates/core/tests/errors/react-server-components/server-graph/react-dom-server-client/input.js @@ -0,0 +1,15 @@ +// This is a comment. + +"use strict"; + +/** + * This is a comment. + */ + +import "react-dom/server" + +import "react-dom/client" + +export default function () { + return null; +} diff --git a/packages/next-swc/crates/core/tests/errors/react-server-components/server-graph/react-dom-server-client/output.js b/packages/next-swc/crates/core/tests/errors/react-server-components/server-graph/react-dom-server-client/output.js new file mode 100644 index 000000000000..6dccfaf7bbaa --- /dev/null +++ b/packages/next-swc/crates/core/tests/errors/react-server-components/server-graph/react-dom-server-client/output.js @@ -0,0 +1,9 @@ +// This is a comment. +"use strict"; +/** + * This is a comment. + */ import "react-dom/server"; +import "react-dom/client"; +export default function() { + return null; +} diff --git a/packages/next-swc/crates/core/tests/errors/react-server-components/server-graph/react-dom-server-client/output.stderr b/packages/next-swc/crates/core/tests/errors/react-server-components/server-graph/react-dom-server-client/output.stderr new file mode 100644 index 000000000000..8e4f211415c9 --- /dev/null +++ b/packages/next-swc/crates/core/tests/errors/react-server-components/server-graph/react-dom-server-client/output.stderr @@ -0,0 +1,12 @@ + + x Disallowed import of `react-dom/server` in the Server Components compilation. + ,-[input.js:9:1] + 9 | import "react-dom/server" + : ^^^^^^^^^^^^^^^^^^^^^^^^^ + `---- + + x Disallowed import of `react-dom/client` in the Server Components compilation. + ,-[input.js:11:1] + 11 | import "react-dom/client" + : ^^^^^^^^^^^^^^^^^^^^^^^^^ + `---- diff --git a/packages/next-swc/crates/core/tests/fixture.rs b/packages/next-swc/crates/core/tests/fixture.rs index 0bdcb527860f..ff6b8a230f13 100644 --- a/packages/next-swc/crates/core/tests/fixture.rs +++ b/packages/next-swc/crates/core/tests/fixture.rs @@ -4,6 +4,7 @@ use next_swc::{ next_ssg::next_ssg, page_config::page_config_test, react_remove_properties::remove_properties, + react_server_components::server_components, relay::{relay, Config as RelayConfig, RelayLanguageConfig}, remove_console::remove_console, shake_exports::{shake_exports, Config as ShakeExportsConfig}, @@ -209,3 +210,41 @@ fn shake_exports_fixture_default(input: PathBuf) { &output, ); } + +#[fixture("tests/fixture/react-server-components/server-graph/**/input.js")] +fn react_server_components_server_graph_fixture(input: PathBuf) { + let output = input.parent().unwrap().join("output.js"); + test_fixture( + syntax(), + &|tr| { + server_components( + FileName::Real(PathBuf::from("/some-project/src/some-file.js")), + next_swc::react_server_components::Config::WithOptions( + next_swc::react_server_components::Options { is_server: true }, + ), + tr.comments.as_ref().clone(), + ) + }, + &input, + &output, + ); +} + +#[fixture("tests/fixture/react-server-components/client-graph/**/input.js")] +fn react_server_components_client_graph_fixture(input: PathBuf) { + let output = input.parent().unwrap().join("output.js"); + test_fixture( + syntax(), + &|tr| { + server_components( + FileName::Real(PathBuf::from("/some-project/src/some-file.js")), + next_swc::react_server_components::Config::WithOptions( + next_swc::react_server_components::Options { is_server: false }, + ), + tr.comments.as_ref().clone(), + ) + }, + &input, + &output, + ); +} diff --git a/packages/next-swc/crates/core/tests/fixture/react-server-components/client-graph/client-entry/input.js b/packages/next-swc/crates/core/tests/fixture/react-server-components/client-graph/client-entry/input.js new file mode 100644 index 000000000000..7031f3f5a5a6 --- /dev/null +++ b/packages/next-swc/crates/core/tests/fixture/react-server-components/client-graph/client-entry/input.js @@ -0,0 +1,31 @@ +// This is a comment. + +"use strict"; + +/** + * This is a comment. + */ + +"client"; + +// This is a comment. + +"foo"; + +"client"; + +import "fs" + +"client"; + +"bar"; + +// This is a comment. + +1 + 1; + +"baz"; + +export default function () { + return null; +} diff --git a/packages/next-swc/crates/core/tests/fixture/react-server-components/client-graph/client-entry/output.js b/packages/next-swc/crates/core/tests/fixture/react-server-components/client-graph/client-entry/output.js new file mode 100644 index 000000000000..4429f7234750 --- /dev/null +++ b/packages/next-swc/crates/core/tests/fixture/react-server-components/client-graph/client-entry/output.js @@ -0,0 +1,13 @@ +// This is a comment. +"use strict"; +// This is a comment. +"foo"; +import "fs"; +"client"; +"bar"; +// This is a comment. +1 + 1; +"baz"; +export default function() { + return null; +} diff --git a/packages/next-swc/crates/core/tests/fixture/react-server-components/server-graph/client-entry/input.js b/packages/next-swc/crates/core/tests/fixture/react-server-components/server-graph/client-entry/input.js new file mode 100644 index 000000000000..f68a352330d6 --- /dev/null +++ b/packages/next-swc/crates/core/tests/fixture/react-server-components/server-graph/client-entry/input.js @@ -0,0 +1,27 @@ +// This is a comment. + +"use strict"; + +/** + * This is a comment. + */ + +"client"; + +// This is a comment. + +"random-directive"; + +import "fs" + +"qwerty"; + +// This is a comment. + +1 + 1; + +"sasaya"; + +export default function () { + return null; +} diff --git a/packages/next-swc/crates/core/tests/fixture/react-server-components/server-graph/client-entry/output.js b/packages/next-swc/crates/core/tests/fixture/react-server-components/server-graph/client-entry/output.js new file mode 100644 index 000000000000..6d2e4f9a4d1a --- /dev/null +++ b/packages/next-swc/crates/core/tests/fixture/react-server-components/server-graph/client-entry/output.js @@ -0,0 +1,3 @@ +// This is a comment. +/* __next_internal_client_entry_do_not_use__ */ const { createProxy } = require("private-next-rsc-mod-ref-proxy"); +module.exports = createProxy("/some-project/src/some-file.js"); diff --git a/packages/next-swc/crates/core/tests/full.rs b/packages/next-swc/crates/core/tests/full.rs index 6c9ad7c5c4b2..01d081f57017 100644 --- a/packages/next-swc/crates/core/tests/full.rs +++ b/packages/next-swc/crates/core/tests/full.rs @@ -57,6 +57,7 @@ fn test(input: &Path, minify: bool) { is_page_file: false, is_development: true, is_server: false, + server_components: None, styled_components: Some(assert_json("{}")), remove_console: None, react_remove_properties: None, diff --git a/packages/next/build/swc/options.js b/packages/next/build/swc/options.js index 4516e065c42c..2c7d1c13fd7e 100644 --- a/packages/next/build/swc/options.js +++ b/packages/next/build/swc/options.js @@ -32,6 +32,7 @@ function getBaseSWCOptions({ resolvedBaseUrl, jsConfig, swcCacheDir, + isServerLayer, }) { const parserConfig = getParserOptions({ filename, jsConfig }) const paths = jsConfig?.compilerOptions?.paths @@ -117,6 +118,11 @@ function getBaseSWCOptions({ modularizeImports: nextConfig?.experimental?.modularizeImports, relay: nextConfig?.compiler?.relay, emotion: getEmotionOptions(nextConfig, development), + serverComponents: nextConfig?.experimental?.serverComponents + ? { + isServer: !!isServerLayer, + } + : false, } } @@ -203,6 +209,7 @@ export function getLoaderSWCOptions({ filename, development, isServer, + isServerLayer, pagesDir, isPageFile, hasReactRefresh, @@ -222,6 +229,7 @@ export function getLoaderSWCOptions({ jsConfig, // resolvedBaseUrl, swcCacheDir, + isServerLayer, }) const isNextDist = nextDistPath.test(filename) diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index add0c3b65b63..5c505d69e215 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -11,6 +11,7 @@ import { APP_DIR_ALIAS, SERVER_RUNTIME, WEBPACK_LAYERS, + RSC_MOD_REF_PROXY_ALIAS, } from '../lib/constants' import { fileExists } from '../lib/file-exists' import { CustomRoutes } from '../lib/load-custom-routes.js' @@ -59,7 +60,6 @@ import { withoutRSCExtensions } from './utils' import browserslist from 'next/dist/compiled/browserslist' import loadJsConfig from './load-jsconfig' import { loadBindings } from './swc' -import { clientComponentRegex } from './webpack/loaders/utils' import { AppBuildManifestPlugin } from './webpack/plugins/app-build-manifest-plugin' import { SubresourceIntegrityPlugin } from './webpack/plugins/subresource-integrity-plugin' @@ -873,6 +873,9 @@ export default async function getBaseWebpackConfig( ...(isClient || isEdgeServer ? getOptimizedAliases() : {}), ...getReactProfilingInProduction(), + [RSC_MOD_REF_PROXY_ALIAS]: + 'next/dist/build/webpack/loaders/next-flight-client-loader/module-proxy', + ...(isClient || isEdgeServer ? { [clientResolveRewrites]: hasRewrites @@ -1016,7 +1019,7 @@ export default async function getBaseWebpackConfig( } const notExternalModules = - /^(?:private-next-pages\/|next\/(?:dist\/pages\/|(?:app|document|link|image|future\/image|constants|dynamic|script)$)|string-hash$)/ + /^(?:private-next-pages\/|next\/(?:dist\/pages\/|(?:app|document|link|image|future\/image|constants|dynamic|script)$)|string-hash|private-next-rsc-mod-ref-proxy$)/ if (notExternalModules.test(request)) { return } @@ -1495,13 +1498,13 @@ export default async function getBaseWebpackConfig( loader: 'next-flight-server-loader', }, }, - { - test: clientComponentRegex, - issuerLayer: WEBPACK_LAYERS.server, - use: { - loader: 'next-flight-client-loader', - }, - }, + // { + // test: clientComponentRegex, + // issuerLayer: WEBPACK_LAYERS.server, + // use: { + // loader: 'next-flight-client-loader', + // }, + // }, // _app should be treated as a client component as well as all its dependencies. { test: new RegExp(`_app\\.(${rawPageExtensions.join('|')})$`), @@ -1544,6 +1547,21 @@ export default async function getBaseWebpackConfig( issuerLayer: WEBPACK_LAYERS.middleware, use: getBabelOrSwcLoader(), }, + ...(hasServerComponents + ? [ + { + test: codeCondition.test, + issuerLayer: WEBPACK_LAYERS.server, + use: { + ...defaultLoaders.babel, + options: { + ...defaultLoaders.babel.options, + isServerLayer: true, + }, + }, + }, + ] + : []), { ...codeCondition, use: diff --git a/packages/next/build/webpack/loaders/next-flight-client-loader/index.ts b/packages/next/build/webpack/loaders/next-flight-client-loader/index.ts index f36ed00d6dfb..ba20bbb72229 100644 --- a/packages/next/build/webpack/loaders/next-flight-client-loader/index.ts +++ b/packages/next/build/webpack/loaders/next-flight-client-loader/index.ts @@ -49,7 +49,7 @@ export default async function transformSource( } const output = ` -const { createProxy } = require("next/dist/build/webpack/loaders/next-flight-client-loader/module-proxy")\n +const { createProxy } = require("private-next-rsc-mod-ref-proxy")\n module.exports = createProxy(${JSON.stringify(this.resourcePath)}) ` // Pass empty sourcemap diff --git a/packages/next/build/webpack/loaders/next-swc-loader.js b/packages/next/build/webpack/loaders/next-swc-loader.js index ead059fd7954..2012ddbce264 100644 --- a/packages/next/build/webpack/loaders/next-swc-loader.js +++ b/packages/next/build/webpack/loaders/next-swc-loader.js @@ -38,6 +38,7 @@ async function loaderTransform(parentTrace, source, inputSourceMap) { const { isServer, + isServerLayer, pagesDir, hasReactRefresh, nextConfig, @@ -50,7 +51,8 @@ async function loaderTransform(parentTrace, source, inputSourceMap) { const swcOptions = getLoaderSWCOptions({ pagesDir, filename, - isServer: isServer, + isServer, + isServerLayer, isPageFile, development: this.mode === 'development', hasReactRefresh, diff --git a/packages/next/build/webpack/loaders/utils.ts b/packages/next/build/webpack/loaders/utils.ts index 6537f2943a9d..1bc58a101c1f 100644 --- a/packages/next/build/webpack/loaders/utils.ts +++ b/packages/next/build/webpack/loaders/utils.ts @@ -3,23 +3,14 @@ import { getPageStaticInfo } from '../../analysis/get-page-static-info' export const defaultJsFileExtensions = ['js', 'mjs', 'jsx', 'ts', 'tsx'] const imageExtensions = ['jpg', 'jpeg', 'png', 'webp', 'avif'] const nextClientComponents = [ - 'link', - 'image', - // TODO-APP: check if this affects the regex - 'future/image', - 'head', - 'script', - 'dynamic', + 'dist/client/link', + 'dist/client/image', + 'dist/client/future/image', + 'dist/shared/lib/head', + 'dist/client/script', + 'dist/shared/lib/dynamic', ] -const NEXT_BUILT_IN_CLIENT_RSC_REGEX = new RegExp( - `[\\\\/]next[\\\\/](${nextClientComponents.join('|')})\\.js$` -) - -export function isNextBuiltinClientComponent(resourcePath: string) { - return NEXT_BUILT_IN_CLIENT_RSC_REGEX.test(resourcePath) -} - export function buildExports(moduleExports: any, isESM: boolean) { let ret = '' Object.keys(moduleExports).forEach((key) => { diff --git a/packages/next/client/components/app-router.client.tsx b/packages/next/client/components/app-router.client.tsx index edc4cf2e77dc..5136d5faa20d 100644 --- a/packages/next/client/components/app-router.client.tsx +++ b/packages/next/client/components/app-router.client.tsx @@ -1,3 +1,5 @@ +'client' + import type { PropsWithChildren, ReactElement, ReactNode } from 'react' import React, { useEffect, useMemo, useCallback } from 'react' import { createFromReadableStream } from 'next/dist/compiled/react-server-dom-webpack' diff --git a/packages/next/client/components/hot-reloader.client.tsx b/packages/next/client/components/hot-reloader.client.tsx index f96d3b681d85..dee184d7e135 100644 --- a/packages/next/client/components/hot-reloader.client.tsx +++ b/packages/next/client/components/hot-reloader.client.tsx @@ -1,3 +1,5 @@ +'client' + import { useCallback, useContext, diff --git a/packages/next/client/components/layout-router.client.tsx b/packages/next/client/components/layout-router.client.tsx index c1521955d3a5..5dfa3fb80fd4 100644 --- a/packages/next/client/components/layout-router.client.tsx +++ b/packages/next/client/components/layout-router.client.tsx @@ -1,3 +1,5 @@ +'client' + import React, { useContext, useEffect, useRef } from 'react' import type { ChildProp, diff --git a/packages/next/client/components/render-from-template-context.client.tsx b/packages/next/client/components/render-from-template-context.client.tsx index b3da594dca99..b3d03a1dc9e4 100644 --- a/packages/next/client/components/render-from-template-context.client.tsx +++ b/packages/next/client/components/render-from-template-context.client.tsx @@ -1,3 +1,5 @@ +'client' + import React, { useContext } from 'react' import { TemplateContext } from '../../shared/lib/app-router-context' diff --git a/packages/next/client/future/image.tsx b/packages/next/client/future/image.tsx index 24081c4b7d7a..e42a606e21dc 100644 --- a/packages/next/client/future/image.tsx +++ b/packages/next/client/future/image.tsx @@ -1,3 +1,5 @@ +'client' + import React, { useRef, useEffect, diff --git a/packages/next/client/image.tsx b/packages/next/client/image.tsx index 9b385b4d7094..d986ae3a93c2 100644 --- a/packages/next/client/image.tsx +++ b/packages/next/client/image.tsx @@ -1,3 +1,5 @@ +'client' + import React, { useRef, useEffect, diff --git a/packages/next/client/link.tsx b/packages/next/client/link.tsx index 805dd857bdab..aa7670d53ca0 100644 --- a/packages/next/client/link.tsx +++ b/packages/next/client/link.tsx @@ -1,3 +1,5 @@ +'client' + import React from 'react' import { UrlObject } from 'url' import { diff --git a/packages/next/client/script.tsx b/packages/next/client/script.tsx index b309ffc8224c..affe03743c28 100644 --- a/packages/next/client/script.tsx +++ b/packages/next/client/script.tsx @@ -1,3 +1,5 @@ +'client' + import React, { useEffect, useContext, useRef } from 'react' import { ScriptHTMLAttributes } from 'react' import { HeadManagerContext } from '../shared/lib/head-manager-context' diff --git a/packages/next/lib/constants.ts b/packages/next/lib/constants.ts index f4efa6bfbefd..f4ab54a87b64 100644 --- a/packages/next/lib/constants.ts +++ b/packages/next/lib/constants.ts @@ -13,6 +13,7 @@ export const PAGES_DIR_ALIAS = 'private-next-pages' export const DOT_NEXT_ALIAS = 'private-dot-next' export const ROOT_DIR_ALIAS = 'private-next-root-dir' export const APP_DIR_ALIAS = 'private-next-app-dir' +export const RSC_MOD_REF_PROXY_ALIAS = 'private-next-rsc-mod-ref-proxy' export const PUBLIC_DIR_MIDDLEWARE_CONFLICT = `You can not have a '_next' folder inside of your public folder. This conflicts with the internal '/_next' route. https://nextjs.org/docs/messages/public-next-folder-conflict` diff --git a/packages/next/shared/lib/dynamic.tsx b/packages/next/shared/lib/dynamic.tsx index b79caf271dc4..a3046ba88215 100644 --- a/packages/next/shared/lib/dynamic.tsx +++ b/packages/next/shared/lib/dynamic.tsx @@ -1,3 +1,5 @@ +'client' + import React from 'react' import Loadable from './loadable' diff --git a/packages/next/shared/lib/head.tsx b/packages/next/shared/lib/head.tsx index 51c7f5173920..526bf8c29605 100644 --- a/packages/next/shared/lib/head.tsx +++ b/packages/next/shared/lib/head.tsx @@ -1,3 +1,5 @@ +'client' + import React, { useContext } from 'react' import Effect from './side-effect' import { AmpStateContext } from './amp-context' diff --git a/packages/next/taskfile-swc.js b/packages/next/taskfile-swc.js index f7e8478798cb..869920145112 100644 --- a/packages/next/taskfile-swc.js +++ b/packages/next/taskfile-swc.js @@ -108,9 +108,16 @@ module.exports = function (task) { ...swcOptions, } - const output = yield transform(file.data.toString('utf-8'), options) + const source = file.data.toString('utf-8') + const output = yield transform(source, options) const ext = path.extname(file.base) + // Make sure the output content keeps the `"client"` directive. + // TODO: Remove this once SWC fixes the issue. + if (source.startsWith("'client'")) { + output.code = '"client";\n' + output.code + } + // Replace `.ts|.tsx` with `.js` in files with an extension if (ext) { const extRegex = new RegExp(ext.replace('.', '\\.') + '$', 'i') diff --git a/packages/next/taskfile.js b/packages/next/taskfile.js index c43e759b6102..51c672b32a28 100644 --- a/packages/next/taskfile.js +++ b/packages/next/taskfile.js @@ -1447,6 +1447,7 @@ export async function copy_react_server_dom_webpack(task, opts) { await task .source(require.resolve('react-server-dom-webpack')) .target('compiled/react-server-dom-webpack') + await task .source( join( diff --git a/test/e2e/app-dir/app/app/client-component-route/page.client.js b/test/e2e/app-dir/app/app/client-component-route/page.client.js index 94406195d176..5e2c815adf49 100644 --- a/test/e2e/app-dir/app/app/client-component-route/page.client.js +++ b/test/e2e/app-dir/app/app/client-component-route/page.client.js @@ -1,3 +1,5 @@ +'client' + import { useState, useEffect } from 'react' import style from './style.module.css' diff --git a/test/e2e/app-dir/app/app/client-nested/layout.client.js b/test/e2e/app-dir/app/app/client-nested/layout.client.js index c1639af225e7..506d2370b6e3 100644 --- a/test/e2e/app-dir/app/app/client-nested/layout.client.js +++ b/test/e2e/app-dir/app/app/client-nested/layout.client.js @@ -1,3 +1,5 @@ +'client' + import { useState, useEffect } from 'react' import styles from './style.module.css' diff --git a/test/e2e/app-dir/app/app/client-with-errors/get-server-side-props/page.client.js b/test/e2e/app-dir/app/app/client-with-errors/get-server-side-props/page.client.js index 899f4d9b0697..8ea1ccd88990 100644 --- a/test/e2e/app-dir/app/app/client-with-errors/get-server-side-props/page.client.js +++ b/test/e2e/app-dir/app/app/client-with-errors/get-server-side-props/page.client.js @@ -1,3 +1,5 @@ +'client' + // export function getServerSideProps() { { props: {} } } export default function Page() { diff --git a/test/e2e/app-dir/app/app/client-with-errors/get-static-props/page.client.js b/test/e2e/app-dir/app/app/client-with-errors/get-static-props/page.client.js index 717782b1a38e..70911d5f16fb 100644 --- a/test/e2e/app-dir/app/app/client-with-errors/get-static-props/page.client.js +++ b/test/e2e/app-dir/app/app/client-with-errors/get-static-props/page.client.js @@ -1,3 +1,5 @@ +'client' + // export function getStaticProps() { return { props: {} }} export default function Page() { diff --git a/test/e2e/app-dir/app/app/css/css-client/layout.client.js b/test/e2e/app-dir/app/app/css/css-client/layout.client.js index 2add562cce69..ab9641929819 100644 --- a/test/e2e/app-dir/app/app/css/css-client/layout.client.js +++ b/test/e2e/app-dir/app/app/css/css-client/layout.client.js @@ -1,3 +1,5 @@ +'client' + import './client-layout.css' import Foo from './foo' diff --git a/test/e2e/app-dir/app/app/css/css-client/page.client.js b/test/e2e/app-dir/app/app/css/css-client/page.client.js index 24df05926ff9..1e92db88c592 100644 --- a/test/e2e/app-dir/app/app/css/css-client/page.client.js +++ b/test/e2e/app-dir/app/app/css/css-client/page.client.js @@ -1,3 +1,5 @@ +'client' + import './client-page.css' export default function Page() { diff --git a/test/e2e/app-dir/app/app/css/css-nested/layout.client.js b/test/e2e/app-dir/app/app/css/css-nested/layout.client.js index 8e132c3b51a9..8f2a9f56ada6 100644 --- a/test/e2e/app-dir/app/app/css/css-nested/layout.client.js +++ b/test/e2e/app-dir/app/app/css/css-nested/layout.client.js @@ -1,3 +1,5 @@ +'client' + import './style.css' import styles from './style.module.css' diff --git a/test/e2e/app-dir/app/app/css/css-nested/page.client.js b/test/e2e/app-dir/app/app/css/css-nested/page.client.js index c17431379f96..baf7462f9d6a 100644 --- a/test/e2e/app-dir/app/app/css/css-nested/page.client.js +++ b/test/e2e/app-dir/app/app/css/css-nested/page.client.js @@ -1,3 +1,5 @@ +'client' + export default function Page() { return null } diff --git a/test/e2e/app-dir/app/app/dashboard/client-comp.client.jsx b/test/e2e/app-dir/app/app/dashboard/client-comp.client.jsx index cabed435eada..79c317369816 100644 --- a/test/e2e/app-dir/app/app/dashboard/client-comp.client.jsx +++ b/test/e2e/app-dir/app/app/dashboard/client-comp.client.jsx @@ -1,3 +1,5 @@ +'client' + import styles from './client-comp.module.css' import { useEffect, useState } from 'react' diff --git a/test/e2e/app-dir/app/app/dashboard/index/dynamic-imports/dynamic.client.js b/test/e2e/app-dir/app/app/dashboard/index/dynamic-imports/dynamic.client.js index 8b487da2a4eb..ccaf16110d81 100644 --- a/test/e2e/app-dir/app/app/dashboard/index/dynamic-imports/dynamic.client.js +++ b/test/e2e/app-dir/app/app/dashboard/index/dynamic-imports/dynamic.client.js @@ -1,3 +1,5 @@ +'client' + import dynamic from 'next/dist/client/components/shared/dynamic' const Dynamic = dynamic(() => import('../text-dynamic.client')) diff --git a/test/e2e/app-dir/app/app/dashboard/index/dynamic-imports/react-lazy.client.js b/test/e2e/app-dir/app/app/dashboard/index/dynamic-imports/react-lazy.client.js index 0b1870fdeaf9..85727b55afd7 100644 --- a/test/e2e/app-dir/app/app/dashboard/index/dynamic-imports/react-lazy.client.js +++ b/test/e2e/app-dir/app/app/dashboard/index/dynamic-imports/react-lazy.client.js @@ -1,3 +1,5 @@ +'client' + import { useState, lazy } from 'react' const Lazy = lazy(() => import('../text-lazy.client.js')) diff --git a/test/e2e/app-dir/app/app/dashboard/index/text-dynamic.client.js b/test/e2e/app-dir/app/app/dashboard/index/text-dynamic.client.js index 660b8c5953c6..6661c55534fc 100644 --- a/test/e2e/app-dir/app/app/dashboard/index/text-dynamic.client.js +++ b/test/e2e/app-dir/app/app/dashboard/index/text-dynamic.client.js @@ -1,3 +1,5 @@ +'client' + import { useState } from 'react' import styles from './dynamic.module.css' diff --git a/test/e2e/app-dir/app/app/dashboard/index/text-lazy.client.js b/test/e2e/app-dir/app/app/dashboard/index/text-lazy.client.js index 1c225c38b190..ead59b0aa230 100644 --- a/test/e2e/app-dir/app/app/dashboard/index/text-lazy.client.js +++ b/test/e2e/app-dir/app/app/dashboard/index/text-lazy.client.js @@ -1,3 +1,5 @@ +'client' + import styles from './lazy.module.css' export default function LazyComponent() { diff --git a/test/e2e/app-dir/app/app/error/clientcomponent/error.client.js b/test/e2e/app-dir/app/app/error/clientcomponent/error.client.js index cc0c3b620bfd..57ad9184f1f8 100644 --- a/test/e2e/app-dir/app/app/error/clientcomponent/error.client.js +++ b/test/e2e/app-dir/app/app/error/clientcomponent/error.client.js @@ -1,3 +1,5 @@ +'client' + export default function ErrorBoundary({ error, reset }) { return ( <> diff --git a/test/e2e/app-dir/app/app/error/clientcomponent/page.client.js b/test/e2e/app-dir/app/app/error/clientcomponent/page.client.js index 8c3fe1a72901..5f4e73da9dc1 100644 --- a/test/e2e/app-dir/app/app/error/clientcomponent/page.client.js +++ b/test/e2e/app-dir/app/app/error/clientcomponent/page.client.js @@ -1,3 +1,5 @@ +'client' + import { useState } from 'react' export default function Page() { diff --git a/test/e2e/app-dir/app/app/error/ssr-error-client-component/page.client.js b/test/e2e/app-dir/app/app/error/ssr-error-client-component/page.client.js index 1cca8f6810c8..7743a313b475 100644 --- a/test/e2e/app-dir/app/app/error/ssr-error-client-component/page.client.js +++ b/test/e2e/app-dir/app/app/error/ssr-error-client-component/page.client.js @@ -1,3 +1,5 @@ +'client' + export default function Page() { throw new Error('Error during SSR') } diff --git a/test/e2e/app-dir/app/app/hooks/use-cookies/client/page.client.js b/test/e2e/app-dir/app/app/hooks/use-cookies/client/page.client.js index 36e5fbb96f9f..e387b365aa2c 100644 --- a/test/e2e/app-dir/app/app/hooks/use-cookies/client/page.client.js +++ b/test/e2e/app-dir/app/app/hooks/use-cookies/client/page.client.js @@ -1,3 +1,5 @@ +'client' + import { useCookies } from 'next/dist/client/components/hooks-server' export default function Page() { diff --git a/test/e2e/app-dir/app/app/hooks/use-headers/client/page.client.js b/test/e2e/app-dir/app/app/hooks/use-headers/client/page.client.js index 9fb9b875af8a..022da1e08a93 100644 --- a/test/e2e/app-dir/app/app/hooks/use-headers/client/page.client.js +++ b/test/e2e/app-dir/app/app/hooks/use-headers/client/page.client.js @@ -1,3 +1,5 @@ +'client' + import { useHeaders } from 'next/dist/client/components/hooks-server' export default function Page() { diff --git a/test/e2e/app-dir/app/app/hooks/use-pathname/page.client.js b/test/e2e/app-dir/app/app/hooks/use-pathname/page.client.js index 8aa409b3dd0a..fa8350a8533a 100644 --- a/test/e2e/app-dir/app/app/hooks/use-pathname/page.client.js +++ b/test/e2e/app-dir/app/app/hooks/use-pathname/page.client.js @@ -1,3 +1,5 @@ +'client' + import { usePathname } from 'next/dist/client/components/hooks-client' export default function Page() { diff --git a/test/e2e/app-dir/app/app/hooks/use-preview-data/client/page.client.js b/test/e2e/app-dir/app/app/hooks/use-preview-data/client/page.client.js index 094c66773e73..86fbc8fe7e2e 100644 --- a/test/e2e/app-dir/app/app/hooks/use-preview-data/client/page.client.js +++ b/test/e2e/app-dir/app/app/hooks/use-preview-data/client/page.client.js @@ -1,3 +1,5 @@ +'client' + import { usePreviewData } from 'next/dist/client/components/hooks-server' export default function Page() { diff --git a/test/e2e/app-dir/app/app/hooks/use-router/page.client.js b/test/e2e/app-dir/app/app/hooks/use-router/page.client.js index 844c53fe1e4f..51a14d940b8b 100644 --- a/test/e2e/app-dir/app/app/hooks/use-router/page.client.js +++ b/test/e2e/app-dir/app/app/hooks/use-router/page.client.js @@ -1,3 +1,5 @@ +'client' + import { useRouter } from 'next/dist/client/components/hooks-client' export default function Page() { diff --git a/test/e2e/app-dir/app/app/hooks/use-router/sub-page/page.client.js b/test/e2e/app-dir/app/app/hooks/use-router/sub-page/page.client.js index 14c197c67f46..82c6347e59fb 100644 --- a/test/e2e/app-dir/app/app/hooks/use-router/sub-page/page.client.js +++ b/test/e2e/app-dir/app/app/hooks/use-router/sub-page/page.client.js @@ -1,3 +1,5 @@ +'client' + export default function Page() { return

hello from /hooks/use-router/sub-page

} diff --git a/test/e2e/app-dir/app/app/hooks/use-search-params/page.client.js b/test/e2e/app-dir/app/app/hooks/use-search-params/page.client.js index f16caf12cf84..d9876c9c7b20 100644 --- a/test/e2e/app-dir/app/app/hooks/use-search-params/page.client.js +++ b/test/e2e/app-dir/app/app/hooks/use-search-params/page.client.js @@ -1,3 +1,5 @@ +'client' + import { useSearchParams } from 'next/dist/client/components/hooks-client' export default function Page() { diff --git a/test/e2e/app-dir/app/app/navigation/link.client.js b/test/e2e/app-dir/app/app/navigation/link.client.js index 545e4e9b8464..5c1a21d1cccc 100644 --- a/test/e2e/app-dir/app/app/navigation/link.client.js +++ b/test/e2e/app-dir/app/app/navigation/link.client.js @@ -1,3 +1,5 @@ +'client' + import { useRouter } from 'next/dist/client/components/hooks-client' import React from 'react' import { useEffect } from 'react' diff --git a/test/e2e/app-dir/app/app/old-router/Router.client.js b/test/e2e/app-dir/app/app/old-router/Router.client.js index aaec667cb52d..a33b07f8da42 100644 --- a/test/e2e/app-dir/app/app/old-router/Router.client.js +++ b/test/e2e/app-dir/app/app/old-router/Router.client.js @@ -1,3 +1,5 @@ +'client' + import { useRouter, withRouter } from 'next/router' import IsNull from './IsNull' diff --git a/test/e2e/app-dir/app/app/param-and-query/[slug]/page.client.js b/test/e2e/app-dir/app/app/param-and-query/[slug]/page.client.js index b564508deb0c..c77875185947 100644 --- a/test/e2e/app-dir/app/app/param-and-query/[slug]/page.client.js +++ b/test/e2e/app-dir/app/app/param-and-query/[slug]/page.client.js @@ -1,3 +1,5 @@ +'client' + export default function Page({ params, searchParams }) { return (

diff --git a/test/e2e/app-dir/app/app/template/clientcomponent/template.client.js b/test/e2e/app-dir/app/app/template/clientcomponent/template.client.js index 3f9960433a4e..1af291539184 100644 --- a/test/e2e/app-dir/app/app/template/clientcomponent/template.client.js +++ b/test/e2e/app-dir/app/app/template/clientcomponent/template.client.js @@ -1,3 +1,5 @@ +'client' + import { useState } from 'react' export default function Template({ children }) { diff --git a/test/e2e/app-dir/index.test.ts b/test/e2e/app-dir/index.test.ts index 2da374ee05a1..37eb9875b9c3 100644 --- a/test/e2e/app-dir/index.test.ts +++ b/test/e2e/app-dir/index.test.ts @@ -1061,7 +1061,7 @@ describe('app dir', () => { }) if (isDev) { - it('should throw an error when getServerSideProps is used', async () => { + it.skip('should throw an error when getServerSideProps is used', async () => { const pageFile = 'app/client-with-errors/get-server-side-props/page.client.js' const content = await next.readFile(pageFile) @@ -1090,7 +1090,7 @@ describe('app dir', () => { ) }) - it('should throw an error when getStaticProps is used', async () => { + it.skip('should throw an error when getStaticProps is used', async () => { const pageFile = 'app/client-with-errors/get-static-props/page.client.js' const content = await next.readFile(pageFile) diff --git a/test/e2e/app-dir/rsc-basic/app/css-in-js/styled-components.client.js b/test/e2e/app-dir/rsc-basic/app/css-in-js/styled-components.client.js index 70888fc5e72b..f96d44c525ab 100644 --- a/test/e2e/app-dir/rsc-basic/app/css-in-js/styled-components.client.js +++ b/test/e2e/app-dir/rsc-basic/app/css-in-js/styled-components.client.js @@ -1,3 +1,5 @@ +'client' + import styled from 'styled-components' const Button = styled.button` diff --git a/test/e2e/app-dir/rsc-basic/app/css-in-js/styled-jsx.client.js b/test/e2e/app-dir/rsc-basic/app/css-in-js/styled-jsx.client.js index 0ba1cec4d0f8..23803a44bacb 100644 --- a/test/e2e/app-dir/rsc-basic/app/css-in-js/styled-jsx.client.js +++ b/test/e2e/app-dir/rsc-basic/app/css-in-js/styled-jsx.client.js @@ -1,3 +1,5 @@ +'client' + import css from 'styled-jsx/css' const buttonStyles = css` diff --git a/test/e2e/app-dir/rsc-basic/app/external-imports/page.client.js b/test/e2e/app-dir/rsc-basic/app/external-imports/page.client.js index d4c83424f5a7..ff20fc52086d 100644 --- a/test/e2e/app-dir/rsc-basic/app/external-imports/page.client.js +++ b/test/e2e/app-dir/rsc-basic/app/external-imports/page.client.js @@ -1,3 +1,5 @@ +'client' + import getType, { named, value, array, obj } from 'non-isomorphic-text' export default function Page() { diff --git a/test/e2e/app-dir/rsc-basic/app/root-style-registry.client.js b/test/e2e/app-dir/rsc-basic/app/root-style-registry.client.js index bef5c5ce392d..02fba3e8ea58 100644 --- a/test/e2e/app-dir/rsc-basic/app/root-style-registry.client.js +++ b/test/e2e/app-dir/rsc-basic/app/root-style-registry.client.js @@ -1,3 +1,5 @@ +'client' + import React from 'react' import { StyleRegistry, createStyleRegistry } from 'styled-jsx' import { ServerStyleSheet, StyleSheetManager } from 'styled-components' diff --git a/test/e2e/app-dir/rsc-basic/components/bar.client.js b/test/e2e/app-dir/rsc-basic/components/bar.client.js index b58488a9c3b5..06aa9e75ec8c 100644 --- a/test/e2e/app-dir/rsc-basic/components/bar.client.js +++ b/test/e2e/app-dir/rsc-basic/components/bar.client.js @@ -1,3 +1,5 @@ +'client' + export default function bar() { return 'bar.client' } diff --git a/test/e2e/app-dir/rsc-basic/components/cjs.client.js b/test/e2e/app-dir/rsc-basic/components/cjs.client.js index d3c078184ab6..5490cbb84afd 100644 --- a/test/e2e/app-dir/rsc-basic/components/cjs.client.js +++ b/test/e2e/app-dir/rsc-basic/components/cjs.client.js @@ -1,3 +1,5 @@ +'client' + exports.Cjs = function Cjs() { return 'cjs-client' } diff --git a/test/e2e/app-dir/rsc-basic/components/client-exports.client.js b/test/e2e/app-dir/rsc-basic/components/client-exports.client.js index eca931680a23..33a2d942728f 100644 --- a/test/e2e/app-dir/rsc-basic/components/client-exports.client.js +++ b/test/e2e/app-dir/rsc-basic/components/client-exports.client.js @@ -1,3 +1,5 @@ +'client' + export function Named() { return 'named.client' } diff --git a/test/e2e/app-dir/rsc-basic/components/client.client.js b/test/e2e/app-dir/rsc-basic/components/client.client.js index 7b20e208abd7..69b23f3e7ddd 100644 --- a/test/e2e/app-dir/rsc-basic/components/client.client.js +++ b/test/e2e/app-dir/rsc-basic/components/client.client.js @@ -1,3 +1,5 @@ +'client' + import { useState } from 'react' export default function Client() { diff --git a/test/e2e/app-dir/rsc-basic/components/export-all/index.client.js b/test/e2e/app-dir/rsc-basic/components/export-all/index.client.js index 1087c9872326..9785ef915bb2 100644 --- a/test/e2e/app-dir/rsc-basic/components/export-all/index.client.js +++ b/test/e2e/app-dir/rsc-basic/components/export-all/index.client.js @@ -1 +1,3 @@ +'client' + export * from './one' diff --git a/test/e2e/app-dir/rsc-basic/components/foo.client.js b/test/e2e/app-dir/rsc-basic/components/foo.client.js index 30d385449792..c9a40cba3a5b 100644 --- a/test/e2e/app-dir/rsc-basic/components/foo.client.js +++ b/test/e2e/app-dir/rsc-basic/components/foo.client.js @@ -1,3 +1,5 @@ +'client' + export default function foo() { return 'foo.client' } diff --git a/test/e2e/app-dir/rsc-basic/components/partial-hydration-counter.client.js b/test/e2e/app-dir/rsc-basic/components/partial-hydration-counter.client.js index 9f860a79c153..3e2274feb907 100644 --- a/test/e2e/app-dir/rsc-basic/components/partial-hydration-counter.client.js +++ b/test/e2e/app-dir/rsc-basic/components/partial-hydration-counter.client.js @@ -1,3 +1,5 @@ +'client' + import { useState, useEffect } from 'react' export default function Counter() { diff --git a/test/e2e/app-dir/rsc-basic/components/random-module-instance.client.js b/test/e2e/app-dir/rsc-basic/components/random-module-instance.client.js index 02696a05605b..6b3e418e27a3 100644 --- a/test/e2e/app-dir/rsc-basic/components/random-module-instance.client.js +++ b/test/e2e/app-dir/rsc-basic/components/random-module-instance.client.js @@ -1,3 +1,5 @@ +'client' + import { random } from 'random-module-instance' export default function () { diff --git a/test/e2e/app-dir/rsc-basic/components/red/index.client.js b/test/e2e/app-dir/rsc-basic/components/red/index.client.js index 1bc538c3db8e..d75f857662a6 100644 --- a/test/e2e/app-dir/rsc-basic/components/red/index.client.js +++ b/test/e2e/app-dir/rsc-basic/components/red/index.client.js @@ -1,3 +1,5 @@ +'client' + import styles from './style.module.css' export default function RedText(props) { diff --git a/test/e2e/app-dir/rsc-basic/components/router-path.client.js b/test/e2e/app-dir/rsc-basic/components/router-path.client.js index cbcb0f9ff6c6..40d08dd3c7e5 100644 --- a/test/e2e/app-dir/rsc-basic/components/router-path.client.js +++ b/test/e2e/app-dir/rsc-basic/components/router-path.client.js @@ -1,3 +1,5 @@ +'client' + import { useRouter } from 'next/router' export default () => { diff --git a/test/e2e/app-dir/rsc-basic/components/shared.client.js b/test/e2e/app-dir/rsc-basic/components/shared.client.js index 201654274422..5f4acc8484f1 100644 --- a/test/e2e/app-dir/rsc-basic/components/shared.client.js +++ b/test/e2e/app-dir/rsc-basic/components/shared.client.js @@ -1,3 +1,5 @@ +'client' + import Shared from './shared' export default Shared diff --git a/test/e2e/app-dir/rsc-basic/components/shared.js b/test/e2e/app-dir/rsc-basic/components/shared.js index f66f284b9c8a..8cb90b9a87c5 100644 --- a/test/e2e/app-dir/rsc-basic/components/shared.js +++ b/test/e2e/app-dir/rsc-basic/components/shared.js @@ -1,4 +1,4 @@ -import { useState } from 'react' +import React from 'react' import Client from './client.client' const random = ~~(Math.random() * 10000) @@ -6,7 +6,7 @@ const random = ~~(Math.random() * 10000) export default function Shared() { let isServerComponent try { - useState() + React.useState() isServerComponent = false } catch (e) { isServerComponent = true