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