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