1use std::{
2 fmt::{self, Display},
3 iter::FromIterator,
4 path::PathBuf,
5 rc::Rc,
6 sync::Arc,
7};
8
9use once_cell::sync::Lazy;
10use regex::Regex;
11use rustc_hash::FxHashMap;
12use serde::Deserialize;
13use swc_core::{
14 atoms::{atom, Atom},
15 common::{
16 comments::{Comment, CommentKind, Comments},
17 errors::HANDLER,
18 util::take::Take,
19 FileName, Span, Spanned, DUMMY_SP,
20 },
21 ecma::{
22 ast::*,
23 utils::{prepend_stmts, quote_ident, quote_str, ExprFactory},
24 visit::{
25 noop_visit_mut_type, noop_visit_type, visit_mut_pass, Visit, VisitMut, VisitMutWith,
26 VisitWith,
27 },
28 },
29};
30
31use super::{cjs_finder::contains_cjs, import_analyzer::ImportMap};
32use crate::FxIndexMap;
33
34#[derive(Clone, Debug, Deserialize)]
35#[serde(untagged)]
36pub enum Config {
37 All(bool),
38 WithOptions(Options),
39}
40
41impl Config {
42 pub fn truthy(&self) -> bool {
43 match self {
44 Config::All(b) => *b,
45 Config::WithOptions(_) => true,
46 }
47 }
48}
49
50#[derive(Clone, Debug, Deserialize)]
51#[serde(rename_all = "camelCase")]
52pub struct Options {
53 pub is_react_server_layer: bool,
54 pub cache_components_enabled: bool,
55 pub use_cache_enabled: bool,
56}
57
58struct ReactServerComponents<C: Comments> {
63 is_react_server_layer: bool,
64 cache_components_enabled: bool,
65 use_cache_enabled: bool,
66 filepath: String,
67 app_dir: Option<PathBuf>,
68 comments: C,
69 directive_import_collection: Option<(bool, bool, RcVec<ModuleImports>, RcVec<Atom>)>,
70}
71
72#[derive(Clone, Debug)]
73struct ModuleImports {
74 source: (Atom, Span),
75 specifiers: Vec<(Atom, Span)>,
76}
77
78enum RSCErrorKind {
79 RedundantDirectives(Span),
82 NextRscErrServerImport((String, Span)),
83 NextRscErrClientImport((String, Span)),
84 NextRscErrClientDirective(Span),
85 NextRscErrReactApi((String, Span)),
86 NextRscErrErrorFileServerComponent(Span),
87 NextRscErrClientMetadataExport((String, Span)),
88 NextRscErrConflictMetadataExport((Span, Span)),
89 NextRscErrInvalidApi((String, Span)),
90 NextRscErrDeprecatedApi((String, String, Span)),
91 NextSsrDynamicFalseNotAllowed(Span),
92 NextRscErrIncompatibleRouteSegmentConfig(Span, String, NextConfigProperty),
93}
94
95#[derive(Clone, Debug, Copy)]
96enum NextConfigProperty {
97 CacheComponents,
98 UseCache,
99}
100
101impl Display for NextConfigProperty {
102 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103 match self {
104 NextConfigProperty::CacheComponents => write!(f, "experimental.cacheComponents"),
105 NextConfigProperty::UseCache => write!(f, "experimental.useCache"),
106 }
107 }
108}
109
110enum InvalidExportKind {
111 General,
112 Metadata,
113 RouteSegmentConfig(NextConfigProperty),
114}
115
116impl<C: Comments> VisitMut for ReactServerComponents<C> {
117 noop_visit_mut_type!();
118
119 fn visit_mut_module(&mut self, module: &mut Module) {
120 let mut validator = ReactServerComponentValidator::new(
122 self.is_react_server_layer,
123 self.cache_components_enabled,
124 self.use_cache_enabled,
125 self.filepath.clone(),
126 self.app_dir.clone(),
127 );
128
129 module.visit_with(&mut validator);
130 self.directive_import_collection = validator.directive_import_collection;
131
132 let is_client_entry = self
133 .directive_import_collection
134 .as_ref()
135 .expect("directive_import_collection must be set")
136 .0;
137
138 self.remove_top_level_directive(module);
139
140 let is_cjs = contains_cjs(module);
141
142 if self.is_react_server_layer {
143 if is_client_entry {
144 self.to_module_ref(module, is_cjs);
145 return;
146 }
147 } else if is_client_entry {
148 self.prepend_comment_node(module, is_cjs);
149 }
150 module.visit_mut_children_with(self)
151 }
152}
153
154impl<C: Comments> ReactServerComponents<C> {
155 fn remove_top_level_directive(&mut self, module: &mut Module) {
157 module.body.retain(|item| {
158 if let ModuleItem::Stmt(stmt) = item {
159 if let Some(expr_stmt) = stmt.as_expr() {
160 if let Expr::Lit(Lit::Str(Str { value, .. })) = &*expr_stmt.expr {
161 if &**value == "use client" {
162 return false;
164 }
165 }
166 }
167 }
168 true
169 });
170 }
171
172 fn to_module_ref(&self, module: &mut Module, is_cjs: bool) {
175 module.body.clear();
177
178 let proxy_ident = quote_ident!("createProxy");
179 let filepath = quote_str!(&*self.filepath);
180
181 prepend_stmts(
182 &mut module.body,
183 vec![
184 ModuleItem::Stmt(Stmt::Decl(Decl::Var(Box::new(VarDecl {
185 span: DUMMY_SP,
186 kind: VarDeclKind::Const,
187 decls: vec![VarDeclarator {
188 span: DUMMY_SP,
189 name: Pat::Object(ObjectPat {
190 span: DUMMY_SP,
191 props: vec![ObjectPatProp::Assign(AssignPatProp {
192 span: DUMMY_SP,
193 key: proxy_ident.into(),
194 value: None,
195 })],
196 optional: false,
197 type_ann: None,
198 }),
199 init: Some(Box::new(Expr::Call(CallExpr {
200 span: DUMMY_SP,
201 callee: quote_ident!("require").as_callee(),
202 args: vec![quote_str!("private-next-rsc-mod-ref-proxy").as_arg()],
203 ..Default::default()
204 }))),
205 definite: false,
206 }],
207 ..Default::default()
208 })))),
209 ModuleItem::Stmt(Stmt::Expr(ExprStmt {
210 span: DUMMY_SP,
211 expr: Box::new(Expr::Assign(AssignExpr {
212 span: DUMMY_SP,
213 left: MemberExpr {
214 span: DUMMY_SP,
215 obj: Box::new(Expr::Ident(quote_ident!("module").into())),
216 prop: MemberProp::Ident(quote_ident!("exports")),
217 }
218 .into(),
219 op: op!("="),
220 right: Box::new(Expr::Call(CallExpr {
221 span: DUMMY_SP,
222 callee: quote_ident!("createProxy").as_callee(),
223 args: vec![filepath.as_arg()],
224 ..Default::default()
225 })),
226 })),
227 })),
228 ]
229 .into_iter(),
230 );
231
232 self.prepend_comment_node(module, is_cjs);
233 }
234
235 fn prepend_comment_node(&self, module: &Module, is_cjs: bool) {
236 let export_names = &self
237 .directive_import_collection
238 .as_ref()
239 .expect("directive_import_collection must be set")
240 .3;
241
242 self.comments.add_leading(
245 module.span.lo,
246 Comment {
247 span: DUMMY_SP,
248 kind: CommentKind::Block,
249 text: format!(
250 " __next_internal_client_entry_do_not_use__ {} {} ",
251 join_atoms(export_names),
252 if is_cjs { "cjs" } else { "auto" }
253 )
254 .into(),
255 },
256 );
257 }
258}
259
260fn join_atoms(atoms: &[Atom]) -> String {
261 atoms
262 .iter()
263 .map(|atom| atom.as_ref())
264 .collect::<Vec<_>>()
265 .join(",")
266}
267
268fn report_error(app_dir: &Option<PathBuf>, filepath: &str, error_kind: RSCErrorKind) {
271 let (msg, spans) = match error_kind {
272 RSCErrorKind::RedundantDirectives(span) => (
273 "It's not possible to have both `use client` and `use server` directives in the \
274 same file."
275 .to_string(),
276 vec![span],
277 ),
278 RSCErrorKind::NextRscErrClientDirective(span) => (
279 "The \"use client\" directive must be placed before other expressions. Move it to \
280 the top of the file to resolve this issue."
281 .to_string(),
282 vec![span],
283 ),
284 RSCErrorKind::NextRscErrServerImport((source, span)) => {
285 let msg = match source.as_str() {
286 "react-dom/server" => "You're importing a component that imports react-dom/server. To fix it, render or return the content directly as a Server Component instead for perf and security.\nLearn more: https://nextjs.org/docs/app/building-your-application/rendering".to_string(),
288 "next/router" => "You have a Server Component that imports next/router. Use next/navigation instead.\nLearn more: https://nextjs.org/docs/app/api-reference/functions/use-router".to_string(),
290 _ => format!("You're importing a component that imports {source}. It only works in a Client Component but none of its parents are marked with \"use client\", so they're Server Components by default.\nLearn more: https://nextjs.org/docs/app/building-your-application/rendering")
291 };
292
293 (msg, vec![span])
294 }
295 RSCErrorKind::NextRscErrClientImport((source, span)) => {
296 let is_app_dir = app_dir
297 .as_ref()
298 .map(|app_dir| {
299 if let Some(app_dir) = app_dir.as_os_str().to_str() {
300 filepath.starts_with(app_dir)
301 } else {
302 false
303 }
304 })
305 .unwrap_or_default();
306
307 let msg = if !is_app_dir {
308 format!("You're importing a component that needs \"{source}\". That only works in a Server Component which is not supported in the pages/ directory. Read more: https://nextjs.org/docs/app/building-your-application/rendering/server-components\n\n")
309 } else {
310 format!("You're importing a component that needs \"{source}\". That only works in a Server Component but one of its parents is marked with \"use client\", so it's a Client Component.\nLearn more: https://nextjs.org/docs/app/building-your-application/rendering\n\n")
311 };
312 (msg, vec![span])
313 }
314 RSCErrorKind::NextRscErrReactApi((source, span)) => {
315 let msg = if source == "Component" {
316 "You’re importing a class component. It only works in a Client Component but none of its parents are marked with \"use client\", so they're Server Components by default.\nLearn more: https://nextjs.org/docs/app/building-your-application/rendering/client-components\n\n".to_string()
317 } else {
318 format!("You're importing a component that needs `{source}`. This React Hook only works in a Client Component. To fix, mark the file (or its parent) with the `\"use client\"` directive.\n\n Learn more: https://nextjs.org/docs/app/api-reference/directives/use-client\n\n")
319 };
320
321 (msg, vec![span])
322 },
323 RSCErrorKind::NextRscErrErrorFileServerComponent(span) => {
324 (
325 format!("{filepath} must be a Client Component. Add the \"use client\" directive the top of the file to resolve this issue.\nLearn more: https://nextjs.org/docs/app/api-reference/directives/use-client\n\n"),
326 vec![span]
327 )
328 },
329 RSCErrorKind::NextRscErrClientMetadataExport((source, span)) => {
330 (format!("You are attempting to export \"{source}\" from a component marked with \"use client\", which is disallowed. Either remove the export, or the \"use client\" directive. Read more: https://nextjs.org/docs/app/api-reference/directives/use-client\n\n"), vec![span])
331 },
332 RSCErrorKind::NextRscErrConflictMetadataExport((span1, span2)) => (
333 "\"metadata\" and \"generateMetadata\" cannot be exported at the same time, please keep one of them. Read more: https://nextjs.org/docs/app/api-reference/file-conventions/metadata\n\n".to_string(),
334 vec![span1, span2]
335 ),
336 RSCErrorKind::NextRscErrInvalidApi((source, span)) => (
337 format!("\"{source}\" is not supported in app/. Read more: https://nextjs.org/docs/app/building-your-application/data-fetching\n\n"), vec![span]
338 ),
339 RSCErrorKind::NextRscErrDeprecatedApi((source, item, span)) => match (&*source, &*item) {
340 ("next/server", "ImageResponse") => (
341 "ImageResponse moved from \"next/server\" to \"next/og\" since Next.js 14, please \
342 import from \"next/og\" instead"
343 .to_string(),
344 vec![span],
345 ),
346 _ => (format!("\"{source}\" is deprecated."), vec![span]),
347 },
348 RSCErrorKind::NextSsrDynamicFalseNotAllowed(span) => (
349 "`ssr: false` is not allowed with `next/dynamic` in Server Components. Please move it into a Client Component."
350 .to_string(),
351 vec![span],
352 ),
353 RSCErrorKind::NextRscErrIncompatibleRouteSegmentConfig(span, segment, property) => (
354 format!("Route segment config \"{segment}\" is not compatible with `nextConfig.{property}`. Please remove it."),
355 vec![span],
356 ),
357 };
358
359 HANDLER.with(|handler| handler.struct_span_err(spans, msg.as_str()).emit())
360}
361
362fn collect_top_level_directives_and_imports(
364 app_dir: &Option<PathBuf>,
365 filepath: &str,
366 module: &Module,
367) -> (bool, bool, Vec<ModuleImports>, Vec<Atom>) {
368 let mut imports: Vec<ModuleImports> = vec![];
369 let mut finished_directives = false;
370 let mut is_client_entry = false;
371 let mut is_action_file = false;
372
373 let mut export_names = vec![];
374
375 let _ = &module.body.iter().for_each(|item| {
376 match item {
377 ModuleItem::Stmt(stmt) => {
378 if !stmt.is_expr() {
379 finished_directives = true;
381 }
382
383 match stmt.as_expr() {
384 Some(expr_stmt) => {
385 match &*expr_stmt.expr {
386 Expr::Lit(Lit::Str(Str { value, .. })) => {
387 if &**value == "use client" {
388 if !finished_directives {
389 is_client_entry = true;
390
391 if is_action_file {
392 report_error(
393 app_dir,
394 filepath,
395 RSCErrorKind::RedundantDirectives(expr_stmt.span),
396 );
397 }
398 } else {
399 report_error(
400 app_dir,
401 filepath,
402 RSCErrorKind::NextRscErrClientDirective(expr_stmt.span),
403 );
404 }
405 } else if &**value == "use server" && !finished_directives {
406 is_action_file = true;
407
408 if is_client_entry {
409 report_error(
410 app_dir,
411 filepath,
412 RSCErrorKind::RedundantDirectives(expr_stmt.span),
413 );
414 }
415 }
416 }
417 Expr::Paren(ParenExpr { expr, .. }) => {
421 finished_directives = true;
422 if let Expr::Lit(Lit::Str(Str { value, .. })) = &**expr {
423 if &**value == "use client" {
424 report_error(
425 app_dir,
426 filepath,
427 RSCErrorKind::NextRscErrClientDirective(expr_stmt.span),
428 );
429 }
430 }
431 }
432 _ => {
433 finished_directives = true;
435 }
436 }
437 }
438 None => {
439 finished_directives = true;
441 }
442 }
443 }
444 ModuleItem::ModuleDecl(ModuleDecl::Import(
445 import @ ImportDecl {
446 type_only: false, ..
447 },
448 )) => {
449 let source = import.src.value.clone();
450 let specifiers = import
451 .specifiers
452 .iter()
453 .filter(|specifier| {
454 !matches!(
455 specifier,
456 ImportSpecifier::Named(ImportNamedSpecifier {
457 is_type_only: true,
458 ..
459 })
460 )
461 })
462 .map(|specifier| match specifier {
463 ImportSpecifier::Named(named) => match &named.imported {
464 Some(imported) => match &imported {
465 ModuleExportName::Ident(i) => (i.to_id().0, i.span),
466 ModuleExportName::Str(s) => (s.value.clone(), s.span),
467 },
468 None => (named.local.to_id().0, named.local.span),
469 },
470 ImportSpecifier::Default(d) => (atom!(""), d.span),
471 ImportSpecifier::Namespace(n) => (atom!("*"), n.span),
472 })
473 .collect();
474
475 imports.push(ModuleImports {
476 source: (source, import.span),
477 specifiers,
478 });
479
480 finished_directives = true;
481 }
482 ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(e)) => {
484 for specifier in &e.specifiers {
485 export_names.push(match specifier {
486 ExportSpecifier::Default(_) => atom!("default"),
487 ExportSpecifier::Namespace(_) => atom!("*"),
488 ExportSpecifier::Named(named) => match &named.exported {
489 Some(exported) => match &exported {
490 ModuleExportName::Ident(i) => i.sym.clone(),
491 ModuleExportName::Str(s) => s.value.clone(),
492 },
493 _ => match &named.orig {
494 ModuleExportName::Ident(i) => i.sym.clone(),
495 ModuleExportName::Str(s) => s.value.clone(),
496 },
497 },
498 })
499 }
500 finished_directives = true;
501 }
502 ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl { decl, .. })) => {
503 match decl {
504 Decl::Class(ClassDecl { ident, .. }) => {
505 export_names.push(ident.sym.clone());
506 }
507 Decl::Fn(FnDecl { ident, .. }) => {
508 export_names.push(ident.sym.clone());
509 }
510 Decl::Var(var) => {
511 for decl in &var.decls {
512 if let Pat::Ident(ident) = &decl.name {
513 export_names.push(ident.id.sym.clone());
514 }
515 }
516 }
517 _ => {}
518 }
519 finished_directives = true;
520 }
521 ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(ExportDefaultDecl {
522 decl: _,
523 ..
524 })) => {
525 export_names.push(atom!("default"));
526 finished_directives = true;
527 }
528 ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(ExportDefaultExpr {
529 expr: _,
530 ..
531 })) => {
532 export_names.push(atom!("default"));
533 finished_directives = true;
534 }
535 ModuleItem::ModuleDecl(ModuleDecl::ExportAll(_)) => {
536 export_names.push(atom!("*"));
537 }
538 _ => {
539 finished_directives = true;
540 }
541 }
542 });
543
544 (is_client_entry, is_action_file, imports, export_names)
545}
546
547struct ReactServerComponentValidator {
549 is_react_server_layer: bool,
550 cache_components_enabled: bool,
551 use_cache_enabled: bool,
552 filepath: String,
553 app_dir: Option<PathBuf>,
554 invalid_server_imports: Vec<Atom>,
555 invalid_server_lib_apis_mapping: FxHashMap<&'static str, Vec<&'static str>>,
556 deprecated_apis_mapping: FxHashMap<&'static str, Vec<&'static str>>,
557 invalid_client_imports: Vec<Atom>,
558 invalid_client_lib_apis_mapping: FxHashMap<&'static str, Vec<&'static str>>,
559 pub directive_import_collection: Option<(bool, bool, RcVec<ModuleImports>, RcVec<Atom>)>,
560 imports: ImportMap,
561}
562
563type RcVec<T> = Rc<Vec<T>>;
565
566impl ReactServerComponentValidator {
567 pub fn new(
568 is_react_server_layer: bool,
569 cache_components_enabled: bool,
570 use_cache_enabled: bool,
571 filename: String,
572 app_dir: Option<PathBuf>,
573 ) -> Self {
574 Self {
575 is_react_server_layer,
576 cache_components_enabled,
577 use_cache_enabled,
578 filepath: filename,
579 app_dir,
580 directive_import_collection: None,
581 invalid_server_lib_apis_mapping: FxHashMap::from_iter([
585 (
586 "react",
587 vec![
588 "Component",
589 "createContext",
590 "createFactory",
591 "PureComponent",
592 "useDeferredValue",
593 "useEffect",
594 "useImperativeHandle",
595 "useInsertionEffect",
596 "useLayoutEffect",
597 "useReducer",
598 "useRef",
599 "useState",
600 "useSyncExternalStore",
601 "useTransition",
602 "useOptimistic",
603 "useActionState",
604 "experimental_useOptimistic",
605 ],
606 ),
607 (
608 "react-dom",
609 vec![
610 "flushSync",
611 "unstable_batchedUpdates",
612 "useFormStatus",
613 "useFormState",
614 ],
615 ),
616 (
617 "next/navigation",
618 vec![
619 "useSearchParams",
620 "usePathname",
621 "useSelectedLayoutSegment",
622 "useSelectedLayoutSegments",
623 "useParams",
624 "useRouter",
625 "useServerInsertedHTML",
626 "ServerInsertedHTMLContext",
627 "unstable_isUnrecognizedActionError",
628 ],
629 ),
630 ("next/link", vec!["useLinkStatus"]),
631 ]),
632 deprecated_apis_mapping: FxHashMap::from_iter([("next/server", vec!["ImageResponse"])]),
633
634 invalid_server_imports: vec![
635 Atom::from("client-only"),
636 Atom::from("react-dom/client"),
637 Atom::from("react-dom/server"),
638 Atom::from("next/router"),
639 ],
640
641 invalid_client_imports: vec![
642 Atom::from("server-only"),
643 Atom::from("next/headers"),
644 Atom::from("next/root-params"),
645 ],
646
647 invalid_client_lib_apis_mapping: FxHashMap::from_iter([
648 ("next/server", vec!["after"]),
649 (
650 "next/cache",
651 vec![
652 "revalidatePath",
653 "revalidateTag",
654 "unstable_cacheLife",
656 "unstable_cacheTag",
657 ],
659 ),
660 ]),
661 imports: ImportMap::default(),
662 }
663 }
664
665 fn is_from_node_modules(&self, filepath: &str) -> bool {
666 static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"node_modules[\\/]").unwrap());
667 RE.is_match(filepath)
668 }
669
670 fn is_callee_next_dynamic(&self, callee: &Callee) -> bool {
671 match callee {
672 Callee::Expr(expr) => self.imports.is_import(expr, "next/dynamic", "default"),
673 _ => false,
674 }
675 }
676
677 fn assert_invalid_server_lib_apis(&self, import_source: String, import: &ModuleImports) {
682 let deprecated_apis = self.deprecated_apis_mapping.get(import_source.as_str());
683 if let Some(deprecated_apis) = deprecated_apis {
684 for specifier in &import.specifiers {
685 if deprecated_apis.contains(&specifier.0.as_str()) {
686 report_error(
687 &self.app_dir,
688 &self.filepath,
689 RSCErrorKind::NextRscErrDeprecatedApi((
690 import_source.clone(),
691 specifier.0.to_string(),
692 specifier.1,
693 )),
694 );
695 }
696 }
697 }
698
699 let invalid_apis = self
700 .invalid_server_lib_apis_mapping
701 .get(import_source.as_str());
702 if let Some(invalid_apis) = invalid_apis {
703 for specifier in &import.specifiers {
704 if invalid_apis.contains(&specifier.0.as_str()) {
705 report_error(
706 &self.app_dir,
707 &self.filepath,
708 RSCErrorKind::NextRscErrReactApi((specifier.0.to_string(), specifier.1)),
709 );
710 }
711 }
712 }
713 }
714
715 fn assert_server_graph(&self, imports: &[ModuleImports], module: &Module) {
716 if self.is_from_node_modules(&self.filepath) {
718 return;
719 }
720 for import in imports {
721 let source = import.source.0.clone();
722 let source_str = source.to_string();
723 if self.invalid_server_imports.contains(&source) {
724 report_error(
725 &self.app_dir,
726 &self.filepath,
727 RSCErrorKind::NextRscErrServerImport((source_str.clone(), import.source.1)),
728 );
729 }
730
731 self.assert_invalid_server_lib_apis(source_str, import);
732 }
733
734 self.assert_invalid_api(module, false);
735 self.assert_server_filename(module);
736 }
737
738 fn assert_server_filename(&self, module: &Module) {
739 if self.is_from_node_modules(&self.filepath) {
740 return;
741 }
742 static RE: Lazy<Regex> =
743 Lazy::new(|| Regex::new(r"[\\/]((global-)?error)\.(ts|js)x?$").unwrap());
744
745 let is_error_file = RE.is_match(&self.filepath);
746
747 if is_error_file {
748 if let Some(app_dir) = &self.app_dir {
749 if let Some(app_dir) = app_dir.to_str() {
750 if self.filepath.starts_with(app_dir) {
751 let span = if let Some(first_item) = module.body.first() {
752 first_item.span()
753 } else {
754 module.span
755 };
756
757 report_error(
758 &self.app_dir,
759 &self.filepath,
760 RSCErrorKind::NextRscErrErrorFileServerComponent(span),
761 );
762 }
763 }
764 }
765 }
766 }
767
768 fn assert_client_graph(&self, imports: &[ModuleImports]) {
769 if self.is_from_node_modules(&self.filepath) {
770 return;
771 }
772 for import in imports {
773 let source = &import.source.0;
774
775 if self.invalid_client_imports.contains(source) {
776 report_error(
777 &self.app_dir,
778 &self.filepath,
779 RSCErrorKind::NextRscErrClientImport((source.to_string(), import.source.1)),
780 );
781 }
782
783 let invalid_apis = self.invalid_client_lib_apis_mapping.get(source.as_str());
784 if let Some(invalid_apis) = invalid_apis {
785 for specifier in &import.specifiers {
786 if invalid_apis.contains(&specifier.0.as_str()) {
787 report_error(
788 &self.app_dir,
789 &self.filepath,
790 RSCErrorKind::NextRscErrClientImport((
791 specifier.0.to_string(),
792 specifier.1,
793 )),
794 );
795 }
796 }
797 }
798 }
799 }
800
801 fn assert_invalid_api(&self, module: &Module, is_client_entry: bool) {
802 if self.is_from_node_modules(&self.filepath) {
803 return;
804 }
805 static RE: Lazy<Regex> =
806 Lazy::new(|| Regex::new(r"[\\/](page|layout|route)\.(ts|js)x?$").unwrap());
807 let is_app_entry = RE.is_match(&self.filepath);
808
809 if is_app_entry {
810 let mut possibly_invalid_exports: FxIndexMap<Atom, (InvalidExportKind, Span)> =
811 FxIndexMap::default();
812
813 let mut collect_possibly_invalid_exports =
814 |export_name: &Atom, span: &Span| match &**export_name {
815 "getServerSideProps" | "getStaticProps" => {
816 possibly_invalid_exports
817 .insert(export_name.clone(), (InvalidExportKind::General, *span));
818 }
819 "generateMetadata" | "metadata" => {
820 possibly_invalid_exports
821 .insert(export_name.clone(), (InvalidExportKind::Metadata, *span));
822 }
823 "runtime" => {
824 if self.cache_components_enabled {
825 possibly_invalid_exports.insert(
826 export_name.clone(),
827 (
828 InvalidExportKind::RouteSegmentConfig(
829 NextConfigProperty::CacheComponents,
830 ),
831 *span,
832 ),
833 );
834 } else if self.use_cache_enabled {
835 possibly_invalid_exports.insert(
836 export_name.clone(),
837 (
838 InvalidExportKind::RouteSegmentConfig(
839 NextConfigProperty::UseCache,
840 ),
841 *span,
842 ),
843 );
844 }
845 }
846 "dynamicParams" | "dynamic" | "fetchCache" | "revalidate" => {
847 if self.cache_components_enabled {
848 possibly_invalid_exports.insert(
849 export_name.clone(),
850 (
851 InvalidExportKind::RouteSegmentConfig(
852 NextConfigProperty::CacheComponents,
853 ),
854 *span,
855 ),
856 );
857 }
858 }
859 _ => (),
860 };
861
862 for export in &module.body {
863 match export {
864 ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(export)) => {
865 for specifier in &export.specifiers {
866 if let ExportSpecifier::Named(named) = specifier {
867 match &named.orig {
868 ModuleExportName::Ident(i) => {
869 collect_possibly_invalid_exports(&i.sym, &named.span);
870 }
871 ModuleExportName::Str(s) => {
872 collect_possibly_invalid_exports(&s.value, &named.span);
873 }
874 }
875 }
876 }
877 }
878 ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export)) => match &export.decl {
879 Decl::Fn(f) => {
880 collect_possibly_invalid_exports(&f.ident.sym, &f.ident.span);
881 }
882 Decl::Var(v) => {
883 for decl in &v.decls {
884 if let Pat::Ident(i) = &decl.name {
885 collect_possibly_invalid_exports(&i.sym, &i.span);
886 }
887 }
888 }
889 _ => {}
890 },
891 _ => {}
892 }
893 }
894
895 for (export_name, (kind, span)) in &possibly_invalid_exports {
896 match kind {
897 InvalidExportKind::RouteSegmentConfig(property) => {
898 report_error(
899 &self.app_dir,
900 &self.filepath,
901 RSCErrorKind::NextRscErrIncompatibleRouteSegmentConfig(
902 *span,
903 export_name.to_string(),
904 *property,
905 ),
906 );
907 }
908 InvalidExportKind::Metadata => {
909 if is_client_entry
911 && (export_name == "generateMetadata" || export_name == "metadata")
912 {
913 report_error(
914 &self.app_dir,
915 &self.filepath,
916 RSCErrorKind::NextRscErrClientMetadataExport((
917 export_name.to_string(),
918 *span,
919 )),
920 );
921 }
922 }
925 InvalidExportKind::General => {
926 report_error(
927 &self.app_dir,
928 &self.filepath,
929 RSCErrorKind::NextRscErrInvalidApi((export_name.to_string(), *span)),
930 );
931 }
932 }
933 }
934
935 if !is_client_entry {
937 let export1 = possibly_invalid_exports.get(&atom!("generateMetadata"));
938 let export2 = possibly_invalid_exports.get(&atom!("metadata"));
939
940 if let (Some((_, span1)), Some((_, span2))) = (export1, export2) {
941 report_error(
942 &self.app_dir,
943 &self.filepath,
944 RSCErrorKind::NextRscErrConflictMetadataExport((*span1, *span2)),
945 );
946 }
947 }
948 }
949 }
950
951 fn check_for_next_ssr_false(&self, node: &CallExpr) -> Option<()> {
959 if !self.is_callee_next_dynamic(&node.callee) {
960 return None;
961 }
962
963 let ssr_arg = node.args.get(1)?;
964 let obj = ssr_arg.expr.as_object()?;
965
966 for prop in obj.props.iter().filter_map(|v| v.as_prop()?.as_key_value()) {
967 let is_ssr = match &prop.key {
968 PropName::Ident(IdentName { sym, .. }) => sym == "ssr",
969 PropName::Str(s) => s.value == "ssr",
970 _ => false,
971 };
972
973 if is_ssr {
974 let value = prop.value.as_lit()?;
975 if let Lit::Bool(Bool { value: false, .. }) = value {
976 report_error(
977 &self.app_dir,
978 &self.filepath,
979 RSCErrorKind::NextSsrDynamicFalseNotAllowed(node.span),
980 );
981 }
982 }
983 }
984
985 None
986 }
987}
988
989impl Visit for ReactServerComponentValidator {
990 noop_visit_type!();
991
992 fn visit_script(&mut self, script: &swc_core::ecma::ast::Script) {
995 if script.body.is_empty() {
996 self.visit_module(&Module::dummy());
997 }
998 }
999
1000 fn visit_call_expr(&mut self, node: &CallExpr) {
1001 node.visit_children_with(self);
1002
1003 if self.is_react_server_layer {
1004 self.check_for_next_ssr_false(node);
1005 }
1006 }
1007
1008 fn visit_module(&mut self, module: &Module) {
1009 self.imports = ImportMap::analyze(module);
1010
1011 let (is_client_entry, is_action_file, imports, export_names) =
1012 collect_top_level_directives_and_imports(&self.app_dir, &self.filepath, module);
1013 let imports = Rc::new(imports);
1014 let export_names = Rc::new(export_names);
1015
1016 self.directive_import_collection = Some((
1017 is_client_entry,
1018 is_action_file,
1019 imports.clone(),
1020 export_names,
1021 ));
1022
1023 if self.is_react_server_layer {
1024 if is_client_entry {
1025 return;
1026 } else {
1027 self.assert_server_graph(&imports, module);
1033 }
1034 } else {
1035 if !is_action_file {
1040 self.assert_client_graph(&imports);
1041 self.assert_invalid_api(module, true);
1042 }
1043 }
1044
1045 module.visit_children_with(self);
1046 }
1047}
1048
1049pub fn server_components_assert(
1056 filename: FileName,
1057 config: Config,
1058 app_dir: Option<PathBuf>,
1059) -> impl Visit {
1060 let is_react_server_layer: bool = match &config {
1061 Config::WithOptions(x) => x.is_react_server_layer,
1062 _ => false,
1063 };
1064 let cache_components_enabled: bool = match &config {
1065 Config::WithOptions(x) => x.cache_components_enabled,
1066 _ => false,
1067 };
1068 let use_cache_enabled: bool = match &config {
1069 Config::WithOptions(x) => x.use_cache_enabled,
1070 _ => false,
1071 };
1072 let filename = match filename {
1073 FileName::Custom(path) => format!("<{path}>"),
1074 _ => filename.to_string(),
1075 };
1076 ReactServerComponentValidator::new(
1077 is_react_server_layer,
1078 cache_components_enabled,
1079 use_cache_enabled,
1080 filename,
1081 app_dir,
1082 )
1083}
1084
1085pub fn server_components<C: Comments>(
1088 filename: Arc<FileName>,
1089 config: Config,
1090 comments: C,
1091 app_dir: Option<PathBuf>,
1092) -> impl Pass + VisitMut {
1093 let is_react_server_layer: bool = match &config {
1094 Config::WithOptions(x) => x.is_react_server_layer,
1095 _ => false,
1096 };
1097 let cache_components_enabled: bool = match &config {
1098 Config::WithOptions(x) => x.cache_components_enabled,
1099 _ => false,
1100 };
1101 let use_cache_enabled: bool = match &config {
1102 Config::WithOptions(x) => x.use_cache_enabled,
1103 _ => false,
1104 };
1105 visit_mut_pass(ReactServerComponents {
1106 is_react_server_layer,
1107 cache_components_enabled,
1108 use_cache_enabled,
1109 comments,
1110 filepath: match &*filename {
1111 FileName::Custom(path) => format!("<{path}>"),
1112 _ => filename.to_string(),
1113 },
1114 app_dir,
1115 directive_import_collection: None,
1116 })
1117}