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, Wtf8Atom},
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: (Wtf8Atom, 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) => (imported.atom().into_owned(), imported.span()),
489 None => (named.local.to_id().0, named.local.span),
490 },
491 ImportSpecifier::Default(d) => (atom!(""), d.span),
492 ImportSpecifier::Namespace(n) => (atom!("*"), n.span),
493 })
494 .collect();
495
496 imports.push(ModuleImports {
497 source: (source, import.span),
498 specifiers,
499 });
500
501 finished_directives = true;
502 }
503 ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(e)) => {
505 for specifier in &e.specifiers {
506 export_names.push(match specifier {
507 ExportSpecifier::Default(_) => atom!("default"),
508 ExportSpecifier::Namespace(_) => atom!("*"),
509 ExportSpecifier::Named(named) => named
510 .exported
511 .as_ref()
512 .unwrap_or(&named.orig)
513 .atom()
514 .into_owned(),
515 })
516 }
517 finished_directives = true;
518 }
519 ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl { decl, .. })) => {
520 match decl {
521 Decl::Class(ClassDecl { ident, .. }) => {
522 export_names.push(ident.sym.clone());
523 }
524 Decl::Fn(FnDecl { ident, .. }) => {
525 export_names.push(ident.sym.clone());
526 }
527 Decl::Var(var) => {
528 for decl in &var.decls {
529 if let Pat::Ident(ident) = &decl.name {
530 export_names.push(ident.id.sym.clone());
531 }
532 }
533 }
534 _ => {}
535 }
536 finished_directives = true;
537 }
538 ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(ExportDefaultDecl {
539 decl: _,
540 ..
541 })) => {
542 export_names.push(atom!("default"));
543 finished_directives = true;
544 }
545 ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(ExportDefaultExpr {
546 expr: _,
547 ..
548 })) => {
549 export_names.push(atom!("default"));
550 finished_directives = true;
551 }
552 ModuleItem::ModuleDecl(ModuleDecl::ExportAll(_)) => {
553 export_names.push(atom!("*"));
554 }
555 _ => {
556 finished_directives = true;
557 }
558 }
559 });
560
561 let directive = if is_client_entry {
562 Some(ModuleDirective::UseClient)
563 } else if is_action_file {
564 Some(ModuleDirective::UseServer)
565 } else if is_cache_file {
566 Some(ModuleDirective::UseCache)
567 } else {
568 None
569 };
570
571 (directive, imports, export_names)
572}
573
574struct ReactServerComponentValidator {
576 is_react_server_layer: bool,
577 cache_components_enabled: bool,
578 use_cache_enabled: bool,
579 filepath: String,
580 app_dir: Option<PathBuf>,
581 invalid_server_imports: Vec<Wtf8Atom>,
582 invalid_server_lib_apis_mapping: FxHashMap<Wtf8Atom, Vec<&'static str>>,
583 deprecated_apis_mapping: FxHashMap<Wtf8Atom, Vec<&'static str>>,
584 invalid_client_imports: Vec<Wtf8Atom>,
585 invalid_client_lib_apis_mapping: FxHashMap<Wtf8Atom, Vec<&'static str>>,
586 pub module_directive: Option<ModuleDirective>,
587 pub export_names: Vec<Atom>,
588 imports: ImportMap,
589}
590
591impl ReactServerComponentValidator {
592 pub fn new(
593 is_react_server_layer: bool,
594 cache_components_enabled: bool,
595 use_cache_enabled: bool,
596 filename: String,
597 app_dir: Option<PathBuf>,
598 ) -> Self {
599 Self {
600 is_react_server_layer,
601 cache_components_enabled,
602 use_cache_enabled,
603 filepath: filename,
604 app_dir,
605 module_directive: None,
606 export_names: vec![],
607 invalid_server_lib_apis_mapping: FxHashMap::from_iter([
611 (
612 atom!("react").into(),
613 vec![
614 "Component",
615 "createContext",
616 "createFactory",
617 "PureComponent",
618 "useDeferredValue",
619 "useEffect",
620 "useImperativeHandle",
621 "useInsertionEffect",
622 "useLayoutEffect",
623 "useReducer",
624 "useRef",
625 "useState",
626 "useSyncExternalStore",
627 "useTransition",
628 "useOptimistic",
629 "useActionState",
630 "experimental_useOptimistic",
631 ],
632 ),
633 (
634 atom!("react-dom").into(),
635 vec![
636 "flushSync",
637 "unstable_batchedUpdates",
638 "useFormStatus",
639 "useFormState",
640 ],
641 ),
642 (
643 atom!("next/navigation").into(),
644 vec![
645 "useSearchParams",
646 "usePathname",
647 "useSelectedLayoutSegment",
648 "useSelectedLayoutSegments",
649 "useParams",
650 "useRouter",
651 "useServerInsertedHTML",
652 "ServerInsertedHTMLContext",
653 "unstable_isUnrecognizedActionError",
654 ],
655 ),
656 (atom!("next/link").into(), vec!["useLinkStatus"]),
657 ]),
658 deprecated_apis_mapping: FxHashMap::from_iter([(
659 atom!("next/server").into(),
660 vec!["ImageResponse"],
661 )]),
662
663 invalid_server_imports: vec![
664 atom!("client-only").into(),
665 atom!("react-dom/client").into(),
666 atom!("react-dom/server").into(),
667 atom!("next/router").into(),
668 ],
669
670 invalid_client_imports: vec![
671 atom!("server-only").into(),
672 atom!("next/headers").into(),
673 atom!("next/root-params").into(),
674 ],
675
676 invalid_client_lib_apis_mapping: FxHashMap::from_iter([
677 (atom!("next/server").into(), vec!["after"]),
678 (
679 atom!("next/cache").into(),
680 vec![
681 "revalidatePath",
682 "revalidateTag",
683 "cacheLife",
685 "unstable_cacheLife",
686 "cacheTag",
687 "unstable_cacheTag",
688 ],
690 ),
691 ]),
692 imports: ImportMap::default(),
693 }
694 }
695
696 fn is_from_node_modules(&self, filepath: &str) -> bool {
697 static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"node_modules[\\/]").unwrap());
698 RE.is_match(filepath)
699 }
700
701 fn is_callee_next_dynamic(&self, callee: &Callee) -> bool {
702 match callee {
703 Callee::Expr(expr) => self.imports.is_import(expr, "next/dynamic", "default"),
704 _ => false,
705 }
706 }
707
708 fn assert_invalid_server_lib_apis(&self, import_source: &Wtf8Atom, import: &ModuleImports) {
713 let deprecated_apis = self.deprecated_apis_mapping.get(import_source);
714 if let Some(deprecated_apis) = deprecated_apis {
715 for specifier in &import.specifiers {
716 if deprecated_apis.contains(&specifier.0.as_str()) {
717 report_error(
718 &self.app_dir,
719 &self.filepath,
720 RSCErrorKind::NextRscErrDeprecatedApi((
721 import_source.to_string_lossy().into_owned(),
722 specifier.0.to_string(),
723 specifier.1,
724 )),
725 );
726 }
727 }
728 }
729
730 let invalid_apis = self.invalid_server_lib_apis_mapping.get(import_source);
731 if let Some(invalid_apis) = invalid_apis {
732 for specifier in &import.specifiers {
733 if invalid_apis.contains(&specifier.0.as_str()) {
734 report_error(
735 &self.app_dir,
736 &self.filepath,
737 RSCErrorKind::NextRscErrReactApi((specifier.0.to_string(), specifier.1)),
738 );
739 }
740 }
741 }
742 }
743
744 fn assert_server_graph(&self, imports: &[ModuleImports], module: &Module) {
745 if self.is_from_node_modules(&self.filepath) {
747 return;
748 }
749 for import in imports {
750 let source = &import.source.0;
751 if self.invalid_server_imports.contains(source) {
752 report_error(
753 &self.app_dir,
754 &self.filepath,
755 RSCErrorKind::NextRscErrServerImport((
756 source.to_string_lossy().into_owned(),
757 import.source.1,
758 )),
759 );
760 }
761
762 self.assert_invalid_server_lib_apis(source, import);
763 }
764
765 self.assert_invalid_api(module, false);
766 self.assert_server_filename(module);
767 }
768
769 fn assert_server_filename(&self, module: &Module) {
770 if self.is_from_node_modules(&self.filepath) {
771 return;
772 }
773 static RE: Lazy<Regex> =
774 Lazy::new(|| Regex::new(r"[\\/]((global-)?error)\.(ts|js)x?$").unwrap());
775
776 let is_error_file = RE.is_match(&self.filepath);
777
778 if is_error_file {
779 if let Some(app_dir) = &self.app_dir {
780 if let Some(app_dir) = app_dir.to_str() {
781 if self.filepath.starts_with(app_dir) {
782 let span = if let Some(first_item) = module.body.first() {
783 first_item.span()
784 } else {
785 module.span
786 };
787
788 report_error(
789 &self.app_dir,
790 &self.filepath,
791 RSCErrorKind::NextRscErrErrorFileServerComponent(span),
792 );
793 }
794 }
795 }
796 }
797 }
798
799 fn assert_client_graph(&self, imports: &[ModuleImports]) {
800 if self.is_from_node_modules(&self.filepath) {
801 return;
802 }
803 for import in imports {
804 let source = &import.source.0;
805
806 if self.invalid_client_imports.contains(source) {
807 report_error(
808 &self.app_dir,
809 &self.filepath,
810 RSCErrorKind::NextRscErrClientImport((
811 source.to_string_lossy().into_owned(),
812 import.source.1,
813 )),
814 );
815 }
816
817 let invalid_apis = self.invalid_client_lib_apis_mapping.get(source);
818 if let Some(invalid_apis) = invalid_apis {
819 for specifier in &import.specifiers {
820 if invalid_apis.contains(&specifier.0.as_str()) {
821 report_error(
822 &self.app_dir,
823 &self.filepath,
824 RSCErrorKind::NextRscErrClientImport((
825 specifier.0.to_string(),
826 specifier.1,
827 )),
828 );
829 }
830 }
831 }
832 }
833 }
834
835 fn assert_invalid_api(&self, module: &Module, is_client_entry: bool) {
836 if self.is_from_node_modules(&self.filepath) {
837 return;
838 }
839 static RE: Lazy<Regex> =
840 Lazy::new(|| Regex::new(r"[\\/](page|layout|route)\.(ts|js)x?$").unwrap());
841 let is_app_entry = RE.is_match(&self.filepath);
842
843 if is_app_entry {
844 let mut possibly_invalid_exports: FxIndexMap<Atom, (InvalidExportKind, Span)> =
845 FxIndexMap::default();
846
847 let mut collect_possibly_invalid_exports =
848 |export_name: &Atom, span: &Span| match &**export_name {
849 "getServerSideProps" | "getStaticProps" => {
850 possibly_invalid_exports
851 .insert(export_name.clone(), (InvalidExportKind::General, *span));
852 }
853 "generateMetadata" | "metadata" => {
854 possibly_invalid_exports
855 .insert(export_name.clone(), (InvalidExportKind::Metadata, *span));
856 }
857 "runtime" => {
858 if self.cache_components_enabled {
859 possibly_invalid_exports.insert(
860 export_name.clone(),
861 (
862 InvalidExportKind::RouteSegmentConfig(
863 NextConfigProperty::CacheComponents,
864 ),
865 *span,
866 ),
867 );
868 } else if self.use_cache_enabled {
869 possibly_invalid_exports.insert(
870 export_name.clone(),
871 (
872 InvalidExportKind::RouteSegmentConfig(
873 NextConfigProperty::UseCache,
874 ),
875 *span,
876 ),
877 );
878 }
879 }
880 "dynamicParams" | "dynamic" | "fetchCache" | "revalidate"
881 | "experimental_ppr" => {
882 if self.cache_components_enabled {
883 possibly_invalid_exports.insert(
884 export_name.clone(),
885 (
886 InvalidExportKind::RouteSegmentConfig(
887 NextConfigProperty::CacheComponents,
888 ),
889 *span,
890 ),
891 );
892 }
893 }
894 _ => (),
895 };
896
897 for export in &module.body {
898 match export {
899 ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(export)) => {
900 for specifier in &export.specifiers {
901 if let ExportSpecifier::Named(named) = specifier {
902 collect_possibly_invalid_exports(&named.orig.atom(), &named.span);
903 }
904 }
905 }
906 ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export)) => match &export.decl {
907 Decl::Fn(f) => {
908 collect_possibly_invalid_exports(&f.ident.sym, &f.ident.span);
909 }
910 Decl::Var(v) => {
911 for decl in &v.decls {
912 if let Pat::Ident(i) = &decl.name {
913 collect_possibly_invalid_exports(&i.sym, &i.span);
914 }
915 }
916 }
917 _ => {}
918 },
919 _ => {}
920 }
921 }
922
923 for (export_name, (kind, span)) in &possibly_invalid_exports {
924 match kind {
925 InvalidExportKind::RouteSegmentConfig(property) => {
926 report_error(
927 &self.app_dir,
928 &self.filepath,
929 RSCErrorKind::NextRscErrIncompatibleRouteSegmentConfig(
930 *span,
931 export_name.to_string(),
932 *property,
933 ),
934 );
935 }
936 InvalidExportKind::Metadata => {
937 if is_client_entry
939 && (export_name == "generateMetadata" || export_name == "metadata")
940 {
941 report_error(
942 &self.app_dir,
943 &self.filepath,
944 RSCErrorKind::NextRscErrClientMetadataExport((
945 export_name.to_string(),
946 *span,
947 )),
948 );
949 }
950 }
953 InvalidExportKind::General => {
954 report_error(
955 &self.app_dir,
956 &self.filepath,
957 RSCErrorKind::NextRscErrInvalidApi((export_name.to_string(), *span)),
958 );
959 }
960 }
961 }
962
963 if !is_client_entry {
965 let export1 = possibly_invalid_exports.get(&atom!("generateMetadata"));
966 let export2 = possibly_invalid_exports.get(&atom!("metadata"));
967
968 if let (Some((_, span1)), Some((_, span2))) = (export1, export2) {
969 report_error(
970 &self.app_dir,
971 &self.filepath,
972 RSCErrorKind::NextRscErrConflictMetadataExport((*span1, *span2)),
973 );
974 }
975 }
976 }
977 }
978
979 fn check_for_next_ssr_false(&self, node: &CallExpr) -> Option<()> {
987 if !self.is_callee_next_dynamic(&node.callee) {
988 return None;
989 }
990
991 let ssr_arg = node.args.get(1)?;
992 let obj = ssr_arg.expr.as_object()?;
993
994 for prop in obj.props.iter().filter_map(|v| v.as_prop()?.as_key_value()) {
995 let is_ssr = match &prop.key {
996 PropName::Ident(IdentName { sym, .. }) => sym == "ssr",
997 PropName::Str(s) => s.value == "ssr",
998 _ => false,
999 };
1000
1001 if is_ssr {
1002 let value = prop.value.as_lit()?;
1003 if let Lit::Bool(Bool { value: false, .. }) = value {
1004 report_error(
1005 &self.app_dir,
1006 &self.filepath,
1007 RSCErrorKind::NextSsrDynamicFalseNotAllowed(node.span),
1008 );
1009 }
1010 }
1011 }
1012
1013 None
1014 }
1015}
1016
1017impl Visit for ReactServerComponentValidator {
1018 noop_visit_type!();
1019
1020 fn visit_script(&mut self, script: &swc_core::ecma::ast::Script) {
1023 if script.body.is_empty() {
1024 self.visit_module(&Module::dummy());
1025 }
1026 }
1027
1028 fn visit_call_expr(&mut self, node: &CallExpr) {
1029 node.visit_children_with(self);
1030
1031 if self.is_react_server_layer {
1032 self.check_for_next_ssr_false(node);
1033 }
1034 }
1035
1036 fn visit_module(&mut self, module: &Module) {
1037 self.imports = ImportMap::analyze(module);
1038
1039 let (directive, imports, export_names) =
1040 collect_module_info(&self.app_dir, &self.filepath, module);
1041 let imports = Rc::new(imports);
1042
1043 self.module_directive = directive;
1044 self.export_names = export_names;
1045
1046 if self.is_react_server_layer {
1047 if directive == Some(ModuleDirective::UseClient) {
1048 return;
1049 } else {
1050 self.assert_server_graph(&imports, module);
1056 }
1057 } else {
1058 if directive != Some(ModuleDirective::UseServer)
1063 && directive != Some(ModuleDirective::UseCache)
1064 {
1065 self.assert_client_graph(&imports);
1066 self.assert_invalid_api(module, true);
1067 }
1068 }
1069
1070 module.visit_children_with(self);
1071 }
1072}
1073
1074pub fn server_components_assert(
1081 filename: FileName,
1082 config: Config,
1083 app_dir: Option<PathBuf>,
1084) -> impl Visit {
1085 let is_react_server_layer: bool = match &config {
1086 Config::WithOptions(x) => x.is_react_server_layer,
1087 _ => false,
1088 };
1089 let cache_components_enabled: bool = match &config {
1090 Config::WithOptions(x) => x.cache_components_enabled,
1091 _ => false,
1092 };
1093 let use_cache_enabled: bool = match &config {
1094 Config::WithOptions(x) => x.use_cache_enabled,
1095 _ => false,
1096 };
1097 let filename = match filename {
1098 FileName::Custom(path) => format!("<{path}>"),
1099 _ => filename.to_string(),
1100 };
1101 ReactServerComponentValidator::new(
1102 is_react_server_layer,
1103 cache_components_enabled,
1104 use_cache_enabled,
1105 filename,
1106 app_dir,
1107 )
1108}
1109
1110pub fn server_components<C: Comments>(
1113 filename: Arc<FileName>,
1114 config: Config,
1115 comments: C,
1116 app_dir: Option<PathBuf>,
1117) -> impl Pass + VisitMut {
1118 let is_react_server_layer: bool = match &config {
1119 Config::WithOptions(x) => x.is_react_server_layer,
1120 _ => false,
1121 };
1122 let cache_components_enabled: bool = match &config {
1123 Config::WithOptions(x) => x.cache_components_enabled,
1124 _ => false,
1125 };
1126 let use_cache_enabled: bool = match &config {
1127 Config::WithOptions(x) => x.use_cache_enabled,
1128 _ => false,
1129 };
1130 visit_mut_pass(ReactServerComponents {
1131 is_react_server_layer,
1132 cache_components_enabled,
1133 use_cache_enabled,
1134 comments,
1135 filepath: match &*filename {
1136 FileName::Custom(path) => format!("<{path}>"),
1137 _ => filename.to_string(),
1138 },
1139 app_dir,
1140 })
1141}