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