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