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 68e1efe0d0745be..4ebe788b24e1ba0 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 000000000000000..b172ea47fc4ed7c --- /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 6af191fe96da39f..3b6996807e5d19b 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 000000000000000..ec63d654d91017f --- /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 000000000000000..b7e34fed0ea2a4e --- /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 000000000000000..02b271d25120dd6 --- /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 000000000000000..889d68cc97f205d --- /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 000000000000000..30b0a47ff472110 --- /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 000000000000000..ec63d654d91017f --- /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 000000000000000..b7e34fed0ea2a4e --- /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 000000000000000..f3d8e080827dc67 --- /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 000000000000000..a9d6953d0b4ccd4 --- /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 000000000000000..39c2869473c37c7 --- /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 000000000000000..dde1083903f3e61 --- /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 000000000000000..d85a1b44633348a --- /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 000000000000000..ca0fe9d0d95e812 --- /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 000000000000000..1e943500e0dd3d4 --- /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 000000000000000..f39cce47429d27a --- /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 000000000000000..6dccfaf7bbaa2c2 --- /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 000000000000000..8e4f211415c98b2 --- /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 0bdcb527860fdbb..ff6b8a230f13cff 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 000000000000000..7031f3f5a5a6a2f --- /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 000000000000000..4429f7234750e95 --- /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 000000000000000..f68a352330d635c --- /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 000000000000000..6d2e4f9a4d1a138 --- /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 6c9ad7c5c4b2c65..01d081f57017734 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 4516e065c42c875..2c7d1c13fd7ed4d 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 add0c3b65b63688..5c505d69e215663 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 f36ed00d6dfbf88..ba20bbb722290fe 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 ead059fd7954134..2012ddbce264c4e 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 6537f2943a9da20..1bc58a101c1fb87 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 edc4cf2e77dc79d..5136d5faa20d700 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 f96d3b681d85bd8..dee184d7e1351a9 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 c1521955d3a5e61..5dfa3fb80fd4b58 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 b3da594dca9988a..b3d03a1dc9e4e56 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 24081c4b7d7ac05..e42a606e21dc32b 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 9b385b4d7094e0e..d986ae3a93c2816 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 805dd857bdab4c6..aa7670d53ca0bc6 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 b309ffc8224ce2b..affe03743c2891f 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 f4efa6bfbefdc51..f4ab54a87b64131 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 b79caf271dc4fca..a3046ba882152b2 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 51c7f5173920465..526bf8c29605013 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 f7e8478798cb04e..8699201451120c3 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 c43e759b61026f2..51c672b32a287bb 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 94406195d17644a..5e2c815adf49a5b 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 c1639af225e7c3b..506d2370b6e3579 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 899f4d9b069765e..8ea1ccd889904c5 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 717782b1a38e32d..70911d5f16fb98d 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 2add562cce69288..ab96419298190cc 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 24df05926ff9e3c..1e92db88c592372 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 8e132c3b51a9e0c..8f2a9f56ada6167 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 c17431379f962fe..baf7462f9d6a3e5 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 cabed435eada1ee..79c317369816f7d 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 8b487da2a4eba82..ccaf16110d81ede 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 0b1870fdeaf9948..85727b55afd73f9 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 660b8c5953c613a..6661c55534fcf44 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 1c225c38b190cbe..ead59b0aa230514 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 cc0c3b620bfd0ca..57ad9184f1f81f1 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 8c3fe1a72901c1d..5f4e73da9dc1301 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 1cca8f6810c8a0b..7743a313b4754bb 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 36e5fbb96f9f54c..e387b365aa2c527 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 9fb9b875af8a1e4..022da1e08a93073 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 8aa409b3dd0a074..fa8350a8533a8b1 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 094c66773e73d6d..86fbc8fe7e2ea07 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 844c53fe1e4f960..51a14d940b8b818 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 14c197c67f46e4d..82c6347e59fb0af 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 f16caf12cf8454a..d9876c9c7b201df 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 545e4e9b8464bfc..5c1a21d1cccc65b 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 aaec667cb52d575..a33b07f8da42ea7 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 b564508deb0c746..c77875185947219 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 3f9960433a4e2aa..1af291539184048 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 2da374ee05a14ae..37eb9875b9c3f88 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 70888fc5e72b3d7..f96d44c525abb70 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 0ba1cec4d0f8508..23803a44bacbf10 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 d4c83424f5a7473..ff20fc52086ddf3 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 bef5c5ce392dd7b..02fba3e8ea58f08 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 b58488a9c3b5c1f..06aa9e75ec8c2c0 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 d3c078184ab64d6..5490cbb84afd4a6 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 eca931680a232be..33a2d942728f2cf 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 7b20e208abd7c2d..69b23f3e7ddd9ce 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 1087c9872326493..9785ef915bb29e1 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 30d3854497926ef..c9a40cba3a5b953 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 9f860a79c153f77..3e2274feb907c00 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 02696a05605b209..6b3e418e27a3937 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 1bc538c3db8e1d1..d75f857662a6640 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 cbcb0f9ff6c6388..40d08dd3c7e57b5 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 201654274422ab9..5f4acc8484f1b23 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 f66f284b9c8a7f9..8cb90b9a87c59d5 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