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", "unstable_rootParams"]),
649 (
650 "next/cache",
651 vec![
652 "revalidatePath",
653 "revalidateTag",
654 "unstable_cacheLife",
656 "unstable_cacheTag",
657 "unstable_expirePath",
658 "unstable_expireTag",
659 ],
661 ),
662 ]),
663 imports: ImportMap::default(),
664 }
665 }
666
667 fn is_from_node_modules(&self, filepath: &str) -> bool {
668 static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"node_modules[\\/]").unwrap());
669 RE.is_match(filepath)
670 }
671
672 fn is_callee_next_dynamic(&self, callee: &Callee) -> bool {
673 match callee {
674 Callee::Expr(expr) => self.imports.is_import(expr, "next/dynamic", "default"),
675 _ => false,
676 }
677 }
678
679 fn assert_invalid_server_lib_apis(&self, import_source: String, import: &ModuleImports) {
684 let deprecated_apis = self.deprecated_apis_mapping.get(import_source.as_str());
685 if let Some(deprecated_apis) = deprecated_apis {
686 for specifier in &import.specifiers {
687 if deprecated_apis.contains(&specifier.0.as_str()) {
688 report_error(
689 &self.app_dir,
690 &self.filepath,
691 RSCErrorKind::NextRscErrDeprecatedApi((
692 import_source.clone(),
693 specifier.0.to_string(),
694 specifier.1,
695 )),
696 );
697 }
698 }
699 }
700
701 let invalid_apis = self
702 .invalid_server_lib_apis_mapping
703 .get(import_source.as_str());
704 if let Some(invalid_apis) = invalid_apis {
705 for specifier in &import.specifiers {
706 if invalid_apis.contains(&specifier.0.as_str()) {
707 report_error(
708 &self.app_dir,
709 &self.filepath,
710 RSCErrorKind::NextRscErrReactApi((specifier.0.to_string(), specifier.1)),
711 );
712 }
713 }
714 }
715 }
716
717 fn assert_server_graph(&self, imports: &[ModuleImports], module: &Module) {
718 if self.is_from_node_modules(&self.filepath) {
720 return;
721 }
722 for import in imports {
723 let source = import.source.0.clone();
724 let source_str = source.to_string();
725 if self.invalid_server_imports.contains(&source) {
726 report_error(
727 &self.app_dir,
728 &self.filepath,
729 RSCErrorKind::NextRscErrServerImport((source_str.clone(), import.source.1)),
730 );
731 }
732
733 self.assert_invalid_server_lib_apis(source_str, import);
734 }
735
736 self.assert_invalid_api(module, false);
737 self.assert_server_filename(module);
738 }
739
740 fn assert_server_filename(&self, module: &Module) {
741 if self.is_from_node_modules(&self.filepath) {
742 return;
743 }
744 static RE: Lazy<Regex> =
745 Lazy::new(|| Regex::new(r"[\\/]((global-)?error)\.(ts|js)x?$").unwrap());
746
747 let is_error_file = RE.is_match(&self.filepath);
748
749 if is_error_file {
750 if let Some(app_dir) = &self.app_dir {
751 if let Some(app_dir) = app_dir.to_str() {
752 if self.filepath.starts_with(app_dir) {
753 let span = if let Some(first_item) = module.body.first() {
754 first_item.span()
755 } else {
756 module.span
757 };
758
759 report_error(
760 &self.app_dir,
761 &self.filepath,
762 RSCErrorKind::NextRscErrErrorFileServerComponent(span),
763 );
764 }
765 }
766 }
767 }
768 }
769
770 fn assert_client_graph(&self, imports: &[ModuleImports]) {
771 if self.is_from_node_modules(&self.filepath) {
772 return;
773 }
774 for import in imports {
775 let source = &import.source.0;
776
777 if self.invalid_client_imports.contains(source) {
778 report_error(
779 &self.app_dir,
780 &self.filepath,
781 RSCErrorKind::NextRscErrClientImport((source.to_string(), import.source.1)),
782 );
783 }
784
785 let invalid_apis = self.invalid_client_lib_apis_mapping.get(source.as_str());
786 if let Some(invalid_apis) = invalid_apis {
787 for specifier in &import.specifiers {
788 if invalid_apis.contains(&specifier.0.as_str()) {
789 report_error(
790 &self.app_dir,
791 &self.filepath,
792 RSCErrorKind::NextRscErrClientImport((
793 specifier.0.to_string(),
794 specifier.1,
795 )),
796 );
797 }
798 }
799 }
800 }
801 }
802
803 fn assert_invalid_api(&self, module: &Module, is_client_entry: bool) {
804 if self.is_from_node_modules(&self.filepath) {
805 return;
806 }
807 static RE: Lazy<Regex> =
808 Lazy::new(|| Regex::new(r"[\\/](page|layout)\.(ts|js)x?$").unwrap());
809 let is_layout_or_page = RE.is_match(&self.filepath);
810
811 if is_layout_or_page {
812 let mut possibly_invalid_exports: FxIndexMap<Atom, (InvalidExportKind, Span)> =
813 FxIndexMap::default();
814
815 let mut collect_possibly_invalid_exports =
816 |export_name: &Atom, span: &Span| match &**export_name {
817 "getServerSideProps" | "getStaticProps" => {
818 possibly_invalid_exports
819 .insert(export_name.clone(), (InvalidExportKind::General, *span));
820 }
821 "generateMetadata" | "metadata" => {
822 possibly_invalid_exports
823 .insert(export_name.clone(), (InvalidExportKind::Metadata, *span));
824 }
825 "runtime" => {
826 if self.cache_components_enabled {
827 possibly_invalid_exports.insert(
828 export_name.clone(),
829 (
830 InvalidExportKind::RouteSegmentConfig(
831 NextConfigProperty::CacheComponents,
832 ),
833 *span,
834 ),
835 );
836 } else if self.use_cache_enabled {
837 possibly_invalid_exports.insert(
838 export_name.clone(),
839 (
840 InvalidExportKind::RouteSegmentConfig(
841 NextConfigProperty::UseCache,
842 ),
843 *span,
844 ),
845 );
846 }
847 }
848 "dynamicParams" | "dynamic" | "fetchCache" | "revalidate" => {
849 if self.cache_components_enabled {
850 possibly_invalid_exports.insert(
851 export_name.clone(),
852 (
853 InvalidExportKind::RouteSegmentConfig(
854 NextConfigProperty::CacheComponents,
855 ),
856 *span,
857 ),
858 );
859 }
860 }
861 _ => (),
862 };
863
864 for export in &module.body {
865 match export {
866 ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(export)) => {
867 for specifier in &export.specifiers {
868 if let ExportSpecifier::Named(named) = specifier {
869 match &named.orig {
870 ModuleExportName::Ident(i) => {
871 collect_possibly_invalid_exports(&i.sym, &named.span);
872 }
873 ModuleExportName::Str(s) => {
874 collect_possibly_invalid_exports(&s.value, &named.span);
875 }
876 }
877 }
878 }
879 }
880 ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export)) => match &export.decl {
881 Decl::Fn(f) => {
882 collect_possibly_invalid_exports(&f.ident.sym, &f.ident.span);
883 }
884 Decl::Var(v) => {
885 for decl in &v.decls {
886 if let Pat::Ident(i) = &decl.name {
887 collect_possibly_invalid_exports(&i.sym, &i.span);
888 }
889 }
890 }
891 _ => {}
892 },
893 _ => {}
894 }
895 }
896
897 for (export_name, (kind, span)) in &possibly_invalid_exports {
898 match kind {
899 InvalidExportKind::RouteSegmentConfig(property) => {
900 report_error(
901 &self.app_dir,
902 &self.filepath,
903 RSCErrorKind::NextRscErrIncompatibleRouteSegmentConfig(
904 *span,
905 export_name.to_string(),
906 *property,
907 ),
908 );
909 }
910 InvalidExportKind::Metadata => {
911 if is_client_entry
913 && (export_name == "generateMetadata" || export_name == "metadata")
914 {
915 report_error(
916 &self.app_dir,
917 &self.filepath,
918 RSCErrorKind::NextRscErrClientMetadataExport((
919 export_name.to_string(),
920 *span,
921 )),
922 );
923 }
924 }
927 InvalidExportKind::General => {
928 report_error(
929 &self.app_dir,
930 &self.filepath,
931 RSCErrorKind::NextRscErrInvalidApi((export_name.to_string(), *span)),
932 );
933 }
934 }
935 }
936
937 if !is_client_entry {
939 let export1 = possibly_invalid_exports.get(&atom!("generateMetadata"));
940 let export2 = possibly_invalid_exports.get(&atom!("metadata"));
941
942 if let (Some((_, span1)), Some((_, span2))) = (export1, export2) {
943 report_error(
944 &self.app_dir,
945 &self.filepath,
946 RSCErrorKind::NextRscErrConflictMetadataExport((*span1, *span2)),
947 );
948 }
949 }
950 }
951 }
952
953 fn check_for_next_ssr_false(&self, node: &CallExpr) -> Option<()> {
961 if !self.is_callee_next_dynamic(&node.callee) {
962 return None;
963 }
964
965 let ssr_arg = node.args.get(1)?;
966 let obj = ssr_arg.expr.as_object()?;
967
968 for prop in obj.props.iter().filter_map(|v| v.as_prop()?.as_key_value()) {
969 let is_ssr = match &prop.key {
970 PropName::Ident(IdentName { sym, .. }) => sym == "ssr",
971 PropName::Str(s) => s.value == "ssr",
972 _ => false,
973 };
974
975 if is_ssr {
976 let value = prop.value.as_lit()?;
977 if let Lit::Bool(Bool { value: false, .. }) = value {
978 report_error(
979 &self.app_dir,
980 &self.filepath,
981 RSCErrorKind::NextSsrDynamicFalseNotAllowed(node.span),
982 );
983 }
984 }
985 }
986
987 None
988 }
989}
990
991impl Visit for ReactServerComponentValidator {
992 noop_visit_type!();
993
994 fn visit_script(&mut self, script: &swc_core::ecma::ast::Script) {
997 if script.body.is_empty() {
998 self.visit_module(&Module::dummy());
999 }
1000 }
1001
1002 fn visit_call_expr(&mut self, node: &CallExpr) {
1003 node.visit_children_with(self);
1004
1005 if self.is_react_server_layer {
1006 self.check_for_next_ssr_false(node);
1007 }
1008 }
1009
1010 fn visit_module(&mut self, module: &Module) {
1011 self.imports = ImportMap::analyze(module);
1012
1013 let (is_client_entry, is_action_file, imports, export_names) =
1014 collect_top_level_directives_and_imports(&self.app_dir, &self.filepath, module);
1015 let imports = Rc::new(imports);
1016 let export_names = Rc::new(export_names);
1017
1018 self.directive_import_collection = Some((
1019 is_client_entry,
1020 is_action_file,
1021 imports.clone(),
1022 export_names,
1023 ));
1024
1025 if self.is_react_server_layer {
1026 if is_client_entry {
1027 return;
1028 } else {
1029 self.assert_server_graph(&imports, module);
1035 }
1036 } else {
1037 if !is_action_file {
1042 self.assert_client_graph(&imports);
1043 self.assert_invalid_api(module, true);
1044 }
1045 }
1046
1047 module.visit_children_with(self);
1048 }
1049}
1050
1051pub fn server_components_assert(
1058 filename: FileName,
1059 config: Config,
1060 app_dir: Option<PathBuf>,
1061) -> impl Visit {
1062 let is_react_server_layer: bool = match &config {
1063 Config::WithOptions(x) => x.is_react_server_layer,
1064 _ => false,
1065 };
1066 let cache_components_enabled: bool = match &config {
1067 Config::WithOptions(x) => x.cache_components_enabled,
1068 _ => false,
1069 };
1070 let use_cache_enabled: bool = match &config {
1071 Config::WithOptions(x) => x.use_cache_enabled,
1072 _ => false,
1073 };
1074 let filename = match filename {
1075 FileName::Custom(path) => format!("<{path}>"),
1076 _ => filename.to_string(),
1077 };
1078 ReactServerComponentValidator::new(
1079 is_react_server_layer,
1080 cache_components_enabled,
1081 use_cache_enabled,
1082 filename,
1083 app_dir,
1084 )
1085}
1086
1087pub fn server_components<C: Comments>(
1090 filename: Arc<FileName>,
1091 config: Config,
1092 comments: C,
1093 app_dir: Option<PathBuf>,
1094) -> impl Pass + VisitMut {
1095 let is_react_server_layer: bool = match &config {
1096 Config::WithOptions(x) => x.is_react_server_layer,
1097 _ => false,
1098 };
1099 let cache_components_enabled: bool = match &config {
1100 Config::WithOptions(x) => x.cache_components_enabled,
1101 _ => false,
1102 };
1103 let use_cache_enabled: bool = match &config {
1104 Config::WithOptions(x) => x.use_cache_enabled,
1105 _ => false,
1106 };
1107 visit_mut_pass(ReactServerComponents {
1108 is_react_server_layer,
1109 cache_components_enabled,
1110 use_cache_enabled,
1111 comments,
1112 filepath: match &*filename {
1113 FileName::Custom(path) => format!("<{path}>"),
1114 _ => filename.to_string(),
1115 },
1116 app_dir,
1117 directive_import_collection: None,
1118 })
1119}