1use std::{
2 fmt::{self, Display},
3 iter::FromIterator,
4 path::PathBuf,
5 rc::Rc,
6 sync::{Arc, LazyLock},
7};
8
9use regex::Regex;
10use rustc_hash::FxHashMap;
11use serde::Deserialize;
12
13fn build_page_extensions_regex(page_extensions: &[String]) -> String {
14 if page_extensions.is_empty() {
15 "(ts|js)x?".to_string()
16 } else {
17 let escaped: Vec<String> = page_extensions
18 .iter()
19 .map(|ext| regex::escape(ext))
20 .collect();
21 format!("({})", escaped.join("|"))
22 }
23}
24use swc_core::{
25 atoms::{Atom, Wtf8Atom, atom},
26 common::{
27 DUMMY_SP, FileName, Span, Spanned,
28 comments::{Comment, CommentKind, Comments},
29 errors::HANDLER,
30 util::take::Take,
31 },
32 ecma::{
33 ast::*,
34 utils::{ExprFactory, prepend_stmts, prop_name_eq, quote_ident, quote_str},
35 visit::{
36 Visit, VisitMut, VisitMutWith, VisitWith, noop_visit_mut_type, noop_visit_type,
37 visit_mut_pass,
38 },
39 },
40};
41
42use super::{cjs_finder::contains_cjs, import_analyzer::ImportMap};
43use crate::FxIndexMap;
44
45#[derive(Clone, Debug, Deserialize)]
46#[serde(untagged)]
47pub enum Config {
48 All(bool),
49 WithOptions(Options),
50}
51
52impl Config {
53 pub fn truthy(&self) -> bool {
54 match self {
55 Config::All(b) => *b,
56 Config::WithOptions(_) => true,
57 }
58 }
59}
60
61#[derive(Clone, Debug, Deserialize)]
62#[serde(rename_all = "camelCase")]
63pub struct Options {
64 pub is_react_server_layer: bool,
65 pub cache_components_enabled: bool,
66 pub use_cache_enabled: bool,
67 #[serde(default)]
68 pub taint_enabled: bool,
69 #[serde(default)]
70 pub page_extensions: Vec<String>,
71 #[serde(default)]
73 pub is_app_router: bool,
74}
75
76struct ReactServerComponents<C: Comments> {
81 is_react_server_layer: bool,
82 cache_components_enabled: bool,
83 use_cache_enabled: bool,
84 taint_enabled: bool,
85 filepath: String,
86 app_dir: Option<PathBuf>,
87 comments: C,
88 page_extensions: Vec<String>,
89 is_app_router: bool,
90}
91
92#[derive(Clone, Debug)]
93struct ModuleImports {
94 source: (Wtf8Atom, Span),
95 specifiers: Vec<(Atom, Span)>,
96}
97
98#[allow(clippy::enum_variant_names)]
99#[derive(Clone, Copy, Debug, PartialEq, Eq)]
100enum ModuleDirective {
101 UseClient,
102 UseServer,
103 UseCache,
104}
105
106enum RSCErrorKind {
107 UseClientWithUseServer(Span),
108 UseClientWithUseCache(Span),
109 NextRscErrServerImport((String, Span)),
110 NextRscErrClientImport {
111 source: String,
112 span: Span,
113 is_app_router: bool,
114 },
115 NextRscErrClientDirective(Span),
116 NextRscErrReactApi((String, Span)),
117 NextRscErrErrorFileServerComponent(Span),
118 NextRscErrClientMetadataExport((String, Span)),
119 NextRscErrConflictMetadataExport((Span, Span)),
120 NextRscErrInvalidApi((String, Span)),
121 NextRscErrDeprecatedApi((String, String, Span)),
122 NextSsrDynamicFalseNotAllowed(Span),
123 NextRscErrIncompatibleRouteSegmentConfig(Span, String, NextConfigProperty),
124 NextRscErrRequiresRouteSegmentConfig(Span, String, NextConfigProperty),
125 NextRscErrTaintWithoutConfig((String, Span)),
126}
127
128#[derive(Clone, Debug, Copy)]
129enum NextConfigProperty {
130 CacheComponents,
131 UseCache,
132}
133
134impl Display for NextConfigProperty {
135 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136 match self {
137 NextConfigProperty::CacheComponents => write!(f, "cacheComponents"),
138 NextConfigProperty::UseCache => write!(f, "experimental.useCache"),
139 }
140 }
141}
142
143enum InvalidExportKind {
144 General,
145 Metadata,
146 RouteSegmentConfig(NextConfigProperty),
147 RequiresRouteSegmentConfig(NextConfigProperty),
148}
149
150impl<C: Comments> VisitMut for ReactServerComponents<C> {
151 noop_visit_mut_type!();
152
153 fn visit_mut_module(&mut self, module: &mut Module) {
154 let mut validator = ReactServerComponentValidator::new(
156 self.is_react_server_layer,
157 self.cache_components_enabled,
158 self.use_cache_enabled,
159 self.taint_enabled,
160 self.filepath.clone(),
161 self.app_dir.clone(),
162 self.page_extensions.clone(),
163 self.is_app_router,
164 );
165
166 module.visit_with(&mut validator);
167
168 let is_client_entry = validator.module_directive == Some(ModuleDirective::UseClient);
169 let export_names = validator.export_names;
170
171 self.remove_top_level_directive(module);
172
173 let is_cjs = contains_cjs(module);
174
175 if self.is_react_server_layer {
176 if is_client_entry {
177 self.to_module_ref(module, is_cjs, &export_names);
178 return;
179 }
180 } else if is_client_entry {
181 self.prepend_comment_node(module, is_cjs, &export_names);
182 }
183 module.visit_mut_children_with(self)
184 }
185}
186
187impl<C: Comments> ReactServerComponents<C> {
188 fn remove_top_level_directive(&mut self, module: &mut Module) {
190 module.body.retain(|item| {
191 if let ModuleItem::Stmt(stmt) = item
192 && let Some(expr_stmt) = stmt.as_expr()
193 && let Expr::Lit(Lit::Str(Str { value, .. })) = &*expr_stmt.expr
194 && &**value == "use client"
195 {
196 return false;
198 }
199 true
200 });
201 }
202
203 fn to_module_ref(&self, module: &mut Module, is_cjs: bool, export_names: &[Atom]) {
206 module.body.clear();
208
209 let proxy_ident = quote_ident!("createProxy");
210 let filepath = quote_str!(&*self.filepath);
211
212 prepend_stmts(
213 &mut module.body,
214 vec![
215 ModuleItem::Stmt(Stmt::Decl(Decl::Var(Box::new(VarDecl {
216 span: DUMMY_SP,
217 kind: VarDeclKind::Const,
218 decls: vec![VarDeclarator {
219 span: DUMMY_SP,
220 name: Pat::Object(ObjectPat {
221 span: DUMMY_SP,
222 props: vec![ObjectPatProp::Assign(AssignPatProp {
223 span: DUMMY_SP,
224 key: proxy_ident.into(),
225 value: None,
226 })],
227 optional: false,
228 type_ann: None,
229 }),
230 init: Some(Box::new(Expr::Call(CallExpr {
231 span: DUMMY_SP,
232 callee: quote_ident!("require").as_callee(),
233 args: vec![quote_str!("private-next-rsc-mod-ref-proxy").as_arg()],
234 ..Default::default()
235 }))),
236 definite: false,
237 }],
238 ..Default::default()
239 })))),
240 ModuleItem::Stmt(Stmt::Expr(ExprStmt {
241 span: DUMMY_SP,
242 expr: Box::new(Expr::Assign(AssignExpr {
243 span: DUMMY_SP,
244 left: MemberExpr {
245 span: DUMMY_SP,
246 obj: Box::new(Expr::Ident(quote_ident!("module").into())),
247 prop: MemberProp::Ident(quote_ident!("exports")),
248 }
249 .into(),
250 op: op!("="),
251 right: Box::new(Expr::Call(CallExpr {
252 span: DUMMY_SP,
253 callee: quote_ident!("createProxy").as_callee(),
254 args: vec![filepath.as_arg()],
255 ..Default::default()
256 })),
257 })),
258 })),
259 ]
260 .into_iter(),
261 );
262
263 self.prepend_comment_node(module, is_cjs, export_names);
264 }
265
266 fn prepend_comment_node(&self, module: &Module, is_cjs: bool, export_names: &[Atom]) {
267 self.comments.add_leading(
270 module.span.lo,
271 Comment {
272 span: DUMMY_SP,
273 kind: CommentKind::Block,
274 text: format!(
275 " __next_internal_client_entry_do_not_use__ {} {} ",
276 join_atoms(export_names),
277 if is_cjs { "cjs" } else { "auto" }
278 )
279 .into(),
280 },
281 );
282 }
283}
284
285fn join_atoms(atoms: &[Atom]) -> String {
286 atoms
287 .iter()
288 .map(|atom| atom.as_ref())
289 .collect::<Vec<_>>()
290 .join(",")
291}
292
293fn is_in_app_dir(app_dir: &Option<PathBuf>, filepath: &str) -> bool {
300 let Some(app_dir) = app_dir.as_ref().and_then(|app_dir| app_dir.to_str()) else {
301 return false;
302 };
303
304 filepath
307 .strip_prefix(app_dir.trim_end_matches(['/', '\\']))
308 .is_some_and(|rest| rest.starts_with(['/', '\\']))
309}
310
311fn report_error(filepath: &str, error_kind: RSCErrorKind) {
314 let (msg, spans) = match error_kind {
315 RSCErrorKind::UseClientWithUseServer(span) => (
316 "It's not possible to have both \"use client\" and \"use server\" directives in the \
317 same file."
318 .to_string(),
319 vec![span],
320 ),
321 RSCErrorKind::UseClientWithUseCache(span) => (
322 "It's not possible to have both \"use client\" and \"use cache\" directives in the \
323 same file."
324 .to_string(),
325 vec![span],
326 ),
327 RSCErrorKind::NextRscErrClientDirective(span) => (
328 "The \"use client\" directive must be placed before other expressions. Move it to \
329 the top of the file to resolve this issue."
330 .to_string(),
331 vec![span],
332 ),
333 RSCErrorKind::NextRscErrServerImport((source, span)) => {
334 let msg = match source.as_str() {
335 "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(),
337 "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(),
339 _ => 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")
340 };
341
342 (msg, vec![span])
343 }
344 RSCErrorKind::NextRscErrClientImport {
345 source,
346 span,
347 is_app_router,
348 } => {
349 let msg = if !is_app_router {
350 format!("You're importing a module that depends on \"{source}\". This API is only available in Server Components in the App Router, but you are using it in the Pages Router.\nLearn more: https://nextjs.org/docs/app/building-your-application/rendering/server-components\n\n")
351 } else {
352 format!("You're importing a module that depends on \"{source}\" into a React Client Component module. This API is only available in Server Components but one of its parents is marked with \"use client\", so this module is also a Client Component.\nLearn more: https://nextjs.org/docs/app/building-your-application/rendering\n\n")
353 };
354 (msg, vec![span])
355 }
356 RSCErrorKind::NextRscErrReactApi((source, span)) => {
357 let msg = if source == "Component" {
358 "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()
359 } else {
360 format!("You're importing a module that depends on `{source}` into a React Server Component module. This API is only available in Client Components. To fix, mark the file (or its parent) with the `\"use client\"` directive.\nLearn more: https://nextjs.org/docs/app/api-reference/directives/use-client\n\n")
361 };
362
363 (msg, vec![span])
364 },
365 RSCErrorKind::NextRscErrErrorFileServerComponent(span) => {
366 (
367 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"),
368 vec![span]
369 )
370 },
371 RSCErrorKind::NextRscErrClientMetadataExport((source, span)) => {
372 (format!("You are attempting to export \"{source}\" from a component marked with \"use client\", which is disallowed. \"{source}\" must be resolved on the server before the page component is rendered. Keep your page as a Server Component and move Client Component logic to a separate file. Read more: https://nextjs.org/docs/app/api-reference/functions/generate-metadata#why-generatemetadata-is-server-component-only\n\n"), vec![span])
373 },
374 RSCErrorKind::NextRscErrConflictMetadataExport((span1, span2)) => (
375 "\"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(),
376 vec![span1, span2]
377 ),
378 RSCErrorKind::NextRscErrInvalidApi((source, span)) => (
379 format!("\"{source}\" is not supported in app/. Read more: https://nextjs.org/docs/app/building-your-application/data-fetching\n\n"), vec![span]
380 ),
381 RSCErrorKind::NextRscErrDeprecatedApi((source, item, span)) => match (&*source, &*item) {
382 ("next/server", "ImageResponse") => (
383 "ImageResponse moved from \"next/server\" to \"next/og\" since Next.js 14, please \
384 import from \"next/og\" instead"
385 .to_string(),
386 vec![span],
387 ),
388 _ => (format!("\"{source}\" is deprecated."), vec![span]),
389 },
390 RSCErrorKind::NextSsrDynamicFalseNotAllowed(span) => (
391 "`ssr: false` is not allowed with `next/dynamic` in Server Components. Please move it into a Client Component."
392 .to_string(),
393 vec![span],
394 ),
395 RSCErrorKind::NextRscErrIncompatibleRouteSegmentConfig(span, segment, property) => (
396 format!("Route segment config \"{segment}\" is not compatible with `nextConfig.{property}`. Please remove it."),
397 vec![span],
398 ),
399 RSCErrorKind::NextRscErrRequiresRouteSegmentConfig(span, segment, property) => (
400 format!("Route segment config \"{segment}\" requires `nextConfig.{property}` to be enabled."),
401 vec![span],
402 ),
403 RSCErrorKind::NextRscErrTaintWithoutConfig((api_name, span)) => (
404 format!(
405 "You're importing `{api_name}` from React which requires `experimental.taint: true` in your Next.js config. Learn more: https://nextjs.org/docs/app/api-reference/config/next-config-js/taint"
406 ),
407 vec![span],
408 ),
409 };
410
411 HANDLER.with(|handler| handler.struct_span_err(spans, msg.as_str()).emit())
412}
413
414fn collect_module_info(
416 filepath: &str,
417 module: &Module,
418) -> (Option<ModuleDirective>, Vec<ModuleImports>, Vec<Atom>) {
419 let mut imports: Vec<ModuleImports> = vec![];
420 let mut finished_directives = false;
421 let mut is_client_entry = false;
422 let mut is_action_file = false;
423 let mut is_cache_file = false;
424
425 let mut export_names = vec![];
426
427 let _ = &module.body.iter().for_each(|item| {
428 match item {
429 ModuleItem::Stmt(stmt) => {
430 if !stmt.is_expr() {
431 finished_directives = true;
433 }
434
435 match stmt.as_expr() {
436 Some(expr_stmt) => {
437 match &*expr_stmt.expr {
438 Expr::Lit(Lit::Str(Str { value, .. })) => {
439 if &**value == "use client" {
440 if !finished_directives {
441 is_client_entry = true;
442
443 if is_action_file {
444 report_error(
445 filepath,
446 RSCErrorKind::UseClientWithUseServer(
447 expr_stmt.span,
448 ),
449 );
450 } else if is_cache_file {
451 report_error(
452 filepath,
453 RSCErrorKind::UseClientWithUseCache(expr_stmt.span),
454 );
455 }
456 } else {
457 report_error(
458 filepath,
459 RSCErrorKind::NextRscErrClientDirective(expr_stmt.span),
460 );
461 }
462 } else if &**value == "use server" && !finished_directives {
463 is_action_file = true;
464
465 if is_client_entry {
466 report_error(
467 filepath,
468 RSCErrorKind::UseClientWithUseServer(expr_stmt.span),
469 );
470 }
471 } else if (&**value == "use cache"
472 || value.starts_with("use cache: "))
473 && !finished_directives
474 {
475 is_cache_file = true;
476
477 if is_client_entry {
478 report_error(
479 filepath,
480 RSCErrorKind::UseClientWithUseCache(expr_stmt.span),
481 );
482 }
483 }
484 }
485 Expr::Paren(ParenExpr { expr, .. }) => {
489 finished_directives = true;
490 if let Expr::Lit(Lit::Str(Str { value, .. })) = &**expr
491 && &**value == "use client"
492 {
493 report_error(
494 filepath,
495 RSCErrorKind::NextRscErrClientDirective(expr_stmt.span),
496 );
497 }
498 }
499 _ => {
500 finished_directives = true;
502 }
503 }
504 }
505 None => {
506 finished_directives = true;
508 }
509 }
510 }
511 ModuleItem::ModuleDecl(ModuleDecl::Import(
512 import @ ImportDecl {
513 type_only: false, ..
514 },
515 )) => {
516 let source = import.src.value.clone();
517 let specifiers = import
518 .specifiers
519 .iter()
520 .filter(|specifier| {
521 !matches!(
522 specifier,
523 ImportSpecifier::Named(ImportNamedSpecifier {
524 is_type_only: true,
525 ..
526 })
527 )
528 })
529 .map(|specifier| match specifier {
530 ImportSpecifier::Named(named) => match &named.imported {
531 Some(imported) => (imported.atom().into_owned(), imported.span()),
532 None => (named.local.to_id().0, named.local.span),
533 },
534 ImportSpecifier::Default(d) => (atom!(""), d.span),
535 ImportSpecifier::Namespace(n) => (atom!("*"), n.span),
536 })
537 .collect();
538
539 imports.push(ModuleImports {
540 source: (source, import.span),
541 specifiers,
542 });
543
544 finished_directives = true;
545 }
546 ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(e)) => {
548 for specifier in &e.specifiers {
549 export_names.push(match specifier {
550 ExportSpecifier::Default(_) => atom!("default"),
551 ExportSpecifier::Namespace(_) => atom!("*"),
552 ExportSpecifier::Named(named) => named
553 .exported
554 .as_ref()
555 .unwrap_or(&named.orig)
556 .atom()
557 .into_owned(),
558 })
559 }
560 finished_directives = true;
561 }
562 ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl { decl, .. })) => {
563 match decl {
564 Decl::Class(ClassDecl { ident, .. }) => {
565 export_names.push(ident.sym.clone());
566 }
567 Decl::Fn(FnDecl { ident, .. }) => {
568 export_names.push(ident.sym.clone());
569 }
570 Decl::Var(var) => {
571 for decl in &var.decls {
572 if let Pat::Ident(ident) = &decl.name {
573 export_names.push(ident.id.sym.clone());
574 }
575 }
576 }
577 _ => {}
578 }
579 finished_directives = true;
580 }
581 ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(ExportDefaultDecl { .. })) => {
582 export_names.push(atom!("default"));
583 finished_directives = true;
584 }
585 ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(ExportDefaultExpr { .. })) => {
586 export_names.push(atom!("default"));
587 finished_directives = true;
588 }
589 ModuleItem::ModuleDecl(ModuleDecl::ExportAll(_)) => {
590 export_names.push(atom!("*"));
591 }
592 _ => {
593 finished_directives = true;
594 }
595 }
596 });
597
598 let directive = if is_client_entry {
599 Some(ModuleDirective::UseClient)
600 } else if is_action_file {
601 Some(ModuleDirective::UseServer)
602 } else if is_cache_file {
603 Some(ModuleDirective::UseCache)
604 } else {
605 None
606 };
607
608 (directive, imports, export_names)
609}
610
611struct ReactServerComponentValidator {
613 is_react_server_layer: bool,
614 cache_components_enabled: bool,
615 use_cache_enabled: bool,
616 taint_enabled: bool,
617 filepath: String,
618 app_dir: Option<PathBuf>,
619 invalid_server_imports: Vec<Wtf8Atom>,
620 invalid_server_lib_apis_mapping: FxHashMap<Wtf8Atom, Vec<&'static str>>,
621 deprecated_apis_mapping: FxHashMap<Wtf8Atom, Vec<&'static str>>,
622 invalid_client_imports: Vec<Wtf8Atom>,
623 invalid_client_lib_apis_mapping: FxHashMap<Wtf8Atom, Vec<&'static str>>,
624 react_taint_apis: Vec<&'static str>,
626 pub module_directive: Option<ModuleDirective>,
627 pub export_names: Vec<Atom>,
628 imports: ImportMap,
629 page_extensions: Vec<String>,
630 is_app_router: bool,
631}
632
633impl ReactServerComponentValidator {
634 #[allow(clippy::too_many_arguments)]
635 pub fn new(
636 is_react_server_layer: bool,
637 cache_components_enabled: bool,
638 use_cache_enabled: bool,
639 taint_enabled: bool,
640 filename: String,
641 app_dir: Option<PathBuf>,
642 page_extensions: Vec<String>,
643 is_app_router: bool,
644 ) -> Self {
645 Self {
646 is_react_server_layer,
647 cache_components_enabled,
648 use_cache_enabled,
649 taint_enabled,
650 filepath: filename,
651 app_dir,
652 module_directive: None,
653 export_names: vec![],
654 invalid_server_lib_apis_mapping: FxHashMap::from_iter([
658 (
659 atom!("react").into(),
660 vec![
661 "Component",
662 "createContext",
663 "createFactory",
664 "PureComponent",
665 "useDeferredValue",
666 "useEffect",
667 "useEffectEvent",
668 "useImperativeHandle",
669 "useInsertionEffect",
670 "useLayoutEffect",
671 "useReducer",
672 "useRef",
673 "useState",
674 "useSyncExternalStore",
675 "useTransition",
676 "useOptimistic",
677 "useActionState",
678 "experimental_useOptimistic",
679 ],
680 ),
681 (
682 atom!("react-dom").into(),
683 vec![
684 "flushSync",
685 "unstable_batchedUpdates",
686 "useFormStatus",
687 "useFormState",
688 ],
689 ),
690 (atom!("next/error").into(), vec!["catchError"]),
691 (
692 atom!("next/navigation").into(),
693 vec![
694 "useSearchParams",
695 "usePathname",
696 "useSelectedLayoutSegment",
697 "useSelectedLayoutSegments",
698 "useParams",
699 "useRouter",
700 "useServerInsertedHTML",
701 "ServerInsertedHTMLContext",
702 "unstable_isUnrecognizedActionError",
703 ],
704 ),
705 (atom!("next/link").into(), vec!["useLinkStatus"]),
706 ]),
707 deprecated_apis_mapping: FxHashMap::from_iter([(
708 atom!("next/server").into(),
709 vec!["ImageResponse"],
710 )]),
711
712 invalid_server_imports: vec![
713 atom!("client-only").into(),
714 atom!("react-dom/client").into(),
715 atom!("react-dom/server").into(),
716 atom!("next/router").into(),
717 ],
718
719 invalid_client_imports: vec![
720 atom!("server-only").into(),
721 atom!("next/headers").into(),
722 atom!("next/root-params").into(),
723 ],
724
725 invalid_client_lib_apis_mapping: FxHashMap::from_iter([
726 (atom!("next/server").into(), vec!["after"]),
727 (
728 atom!("next/cache").into(),
729 vec![
730 "revalidatePath",
731 "revalidateTag",
732 "cacheLife",
734 "unstable_cacheLife",
735 "cacheTag",
736 "unstable_cacheTag",
737 "navigation",
738 "prefetch",
739 ],
741 ),
742 ]),
743 react_taint_apis: vec![
744 "experimental_taintObjectReference",
745 "experimental_taintUniqueValue",
746 ],
747 imports: ImportMap::default(),
748 page_extensions,
749 is_app_router,
750 }
751 }
752
753 fn is_from_node_modules(&self, filepath: &str) -> bool {
754 static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"node_modules[\\/]").unwrap());
755 RE.is_match(filepath)
756 }
757
758 fn is_callee_next_dynamic(&self, callee: &Callee) -> bool {
759 match callee {
760 Callee::Expr(expr) => self.imports.is_import(expr, "next/dynamic", "default"),
761 _ => false,
762 }
763 }
764
765 fn assert_invalid_server_lib_apis(&self, import_source: &Wtf8Atom, import: &ModuleImports) {
770 let deprecated_apis = self.deprecated_apis_mapping.get(import_source);
771 if let Some(deprecated_apis) = deprecated_apis {
772 for specifier in &import.specifiers {
773 if deprecated_apis.contains(&specifier.0.as_str()) {
774 report_error(
775 &self.filepath,
776 RSCErrorKind::NextRscErrDeprecatedApi((
777 import_source.to_string_lossy().into_owned(),
778 specifier.0.to_string(),
779 specifier.1,
780 )),
781 );
782 }
783 }
784 }
785
786 let invalid_apis = self.invalid_server_lib_apis_mapping.get(import_source);
787 if let Some(invalid_apis) = invalid_apis {
788 for specifier in &import.specifiers {
789 if invalid_apis.contains(&specifier.0.as_str()) {
790 report_error(
791 &self.filepath,
792 RSCErrorKind::NextRscErrReactApi((specifier.0.to_string(), specifier.1)),
793 );
794 }
795 }
796 }
797 }
798
799 fn assert_react_taint_apis(&self, imports: &[ModuleImports]) {
801 if self.taint_enabled || self.is_from_node_modules(&self.filepath) {
803 return;
804 }
805
806 for import in imports {
807 let source = &import.source.0;
808 if source.as_str() != Some("react") {
810 continue;
811 }
812
813 for specifier in &import.specifiers {
814 if self.react_taint_apis.contains(&specifier.0.as_str()) {
815 report_error(
816 &self.filepath,
817 RSCErrorKind::NextRscErrTaintWithoutConfig((
818 specifier.0.to_string(),
819 specifier.1,
820 )),
821 );
822 }
823 }
824 }
825 }
826
827 fn assert_server_graph(&self, imports: &[ModuleImports], module: &Module) {
828 if self.is_from_node_modules(&self.filepath) {
830 return;
831 }
832 for import in imports {
833 let source = &import.source.0;
834 if self.invalid_server_imports.contains(source) {
835 report_error(
836 &self.filepath,
837 RSCErrorKind::NextRscErrServerImport((
838 source.to_string_lossy().into_owned(),
839 import.source.1,
840 )),
841 );
842 }
843
844 self.assert_invalid_server_lib_apis(source, import);
845 }
846
847 self.assert_invalid_api(module, false);
848 self.assert_server_filename(module);
849 }
850
851 fn assert_server_filename(&self, module: &Module) {
852 if self.is_from_node_modules(&self.filepath) {
853 return;
854 }
855 let ext_pattern = build_page_extensions_regex(&self.page_extensions);
856 let re = Regex::new(&format!(r"[\\/]((global-)?error)\.{ext_pattern}$")).unwrap();
857
858 let is_error_file = re.is_match(&self.filepath);
859
860 if is_error_file && is_in_app_dir(&self.app_dir, &self.filepath) {
861 let span = if let Some(first_item) = module.body.first() {
862 first_item.span()
863 } else {
864 module.span
865 };
866
867 report_error(
868 &self.filepath,
869 RSCErrorKind::NextRscErrErrorFileServerComponent(span),
870 );
871 }
872 }
873
874 fn assert_client_graph(&self, imports: &[ModuleImports]) {
875 if self.is_from_node_modules(&self.filepath) {
876 return;
877 }
878 for import in imports {
879 let source = &import.source.0;
880
881 if self.invalid_client_imports.contains(source) {
882 report_error(
883 &self.filepath,
884 RSCErrorKind::NextRscErrClientImport {
885 source: source.to_string_lossy().into_owned(),
886 span: import.source.1,
887 is_app_router: self.is_app_router,
888 },
889 );
890 }
891
892 let invalid_apis = self.invalid_client_lib_apis_mapping.get(source);
893 if let Some(invalid_apis) = invalid_apis {
894 for specifier in &import.specifiers {
895 if invalid_apis.contains(&specifier.0.as_str()) {
896 report_error(
897 &self.filepath,
898 RSCErrorKind::NextRscErrClientImport {
899 source: specifier.0.to_string(),
900 span: specifier.1,
901 is_app_router: self.is_app_router,
902 },
903 );
904 }
905 }
906 }
907 }
908 }
909
910 fn assert_invalid_api(&self, module: &Module, is_client_entry: bool) {
911 if self.is_from_node_modules(&self.filepath) {
912 return;
913 }
914 let ext_pattern = build_page_extensions_regex(&self.page_extensions);
915 let re = Regex::new(&format!(
920 r"[\\/](page|layout|route|icon\d?|apple-icon\d?|opengraph-image\d?|twitter-image\d?|sitemap|robots|manifest)\.{ext_pattern}$",
921 ))
922 .unwrap();
923 let is_app_entry =
924 re.is_match(&self.filepath) && is_in_app_dir(&self.app_dir, &self.filepath);
925
926 if is_app_entry {
927 let mut possibly_invalid_exports: FxIndexMap<Atom, (InvalidExportKind, Span)> =
928 FxIndexMap::default();
929
930 let mut collect_possibly_invalid_exports =
931 |export_name: &Atom, span: &Span| match &**export_name {
932 "getServerSideProps" | "getStaticProps" => {
933 possibly_invalid_exports
934 .insert(export_name.clone(), (InvalidExportKind::General, *span));
935 }
936 "generateMetadata" | "metadata" => {
937 possibly_invalid_exports
938 .insert(export_name.clone(), (InvalidExportKind::Metadata, *span));
939 }
940 "runtime" => {
941 if self.cache_components_enabled {
942 possibly_invalid_exports.insert(
943 export_name.clone(),
944 (
945 InvalidExportKind::RouteSegmentConfig(
946 NextConfigProperty::CacheComponents,
947 ),
948 *span,
949 ),
950 );
951 } else if self.use_cache_enabled {
952 possibly_invalid_exports.insert(
953 export_name.clone(),
954 (
955 InvalidExportKind::RouteSegmentConfig(
956 NextConfigProperty::UseCache,
957 ),
958 *span,
959 ),
960 );
961 }
962 }
963 "dynamicParams" | "dynamic" | "fetchCache" | "revalidate"
964 | "experimental_ppr"
965 if self.cache_components_enabled =>
966 {
967 possibly_invalid_exports.insert(
968 export_name.clone(),
969 (
970 InvalidExportKind::RouteSegmentConfig(
971 NextConfigProperty::CacheComponents,
972 ),
973 *span,
974 ),
975 );
976 }
977 "instant" if !self.cache_components_enabled => {
978 possibly_invalid_exports.insert(
979 export_name.clone(),
980 (
981 InvalidExportKind::RequiresRouteSegmentConfig(
982 NextConfigProperty::CacheComponents,
983 ),
984 *span,
985 ),
986 );
987 }
988 _ => (),
989 };
990
991 for export in &module.body {
992 match export {
993 ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(export)) => {
994 for specifier in &export.specifiers {
995 if let ExportSpecifier::Named(named) = specifier {
996 collect_possibly_invalid_exports(&named.orig.atom(), &named.span);
997 }
998 }
999 }
1000 ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export)) => match &export.decl {
1001 Decl::Fn(f) => {
1002 collect_possibly_invalid_exports(&f.ident.sym, &f.ident.span);
1003 }
1004 Decl::Var(v) => {
1005 for decl in &v.decls {
1006 if let Pat::Ident(i) = &decl.name {
1007 collect_possibly_invalid_exports(&i.sym, &i.span);
1008 }
1009 }
1010 }
1011 _ => {}
1012 },
1013 _ => {}
1014 }
1015 }
1016
1017 for (export_name, (kind, span)) in &possibly_invalid_exports {
1018 match kind {
1019 InvalidExportKind::RouteSegmentConfig(property) => {
1020 report_error(
1021 &self.filepath,
1022 RSCErrorKind::NextRscErrIncompatibleRouteSegmentConfig(
1023 *span,
1024 export_name.to_string(),
1025 *property,
1026 ),
1027 );
1028 }
1029 InvalidExportKind::RequiresRouteSegmentConfig(property) => {
1030 report_error(
1031 &self.filepath,
1032 RSCErrorKind::NextRscErrRequiresRouteSegmentConfig(
1033 *span,
1034 export_name.to_string(),
1035 *property,
1036 ),
1037 );
1038 }
1039 InvalidExportKind::Metadata => {
1040 if is_client_entry
1042 && (export_name == "generateMetadata" || export_name == "metadata")
1043 {
1044 report_error(
1045 &self.filepath,
1046 RSCErrorKind::NextRscErrClientMetadataExport((
1047 export_name.to_string(),
1048 *span,
1049 )),
1050 );
1051 }
1052 }
1055 InvalidExportKind::General => {
1056 report_error(
1057 &self.filepath,
1058 RSCErrorKind::NextRscErrInvalidApi((export_name.to_string(), *span)),
1059 );
1060 }
1061 }
1062 }
1063
1064 if !is_client_entry {
1066 let export1 = possibly_invalid_exports.get(&atom!("generateMetadata"));
1067 let export2 = possibly_invalid_exports.get(&atom!("metadata"));
1068
1069 if let (Some((_, span1)), Some((_, span2))) = (export1, export2) {
1070 report_error(
1071 &self.filepath,
1072 RSCErrorKind::NextRscErrConflictMetadataExport((*span1, *span2)),
1073 );
1074 }
1075 }
1076 }
1077 }
1078
1079 fn check_for_next_ssr_false(&self, node: &CallExpr) -> Option<()> {
1087 if !self.is_callee_next_dynamic(&node.callee) {
1088 return None;
1089 }
1090
1091 let ssr_arg = node.args.get(1)?;
1092 let obj = ssr_arg.expr.as_object()?;
1093
1094 for prop in obj.props.iter().filter_map(|v| v.as_prop()?.as_key_value()) {
1095 if prop_name_eq(&prop.key, "ssr") {
1096 let value = prop.value.as_lit()?;
1097 if let Lit::Bool(Bool { value: false, .. }) = value {
1098 report_error(
1099 &self.filepath,
1100 RSCErrorKind::NextSsrDynamicFalseNotAllowed(node.span),
1101 );
1102 }
1103 }
1104 }
1105
1106 None
1107 }
1108}
1109
1110impl Visit for ReactServerComponentValidator {
1111 noop_visit_type!();
1112
1113 fn visit_script(&mut self, script: &swc_core::ecma::ast::Script) {
1116 if script.body.is_empty() {
1117 self.visit_module(&Module::dummy());
1118 }
1119 }
1120
1121 fn visit_call_expr(&mut self, node: &CallExpr) {
1122 node.visit_children_with(self);
1123
1124 if self.is_react_server_layer {
1125 self.check_for_next_ssr_false(node);
1126 }
1127 }
1128
1129 fn visit_module(&mut self, module: &Module) {
1130 self.imports = ImportMap::analyze(module);
1131
1132 let (directive, imports, export_names) = collect_module_info(&self.filepath, module);
1133 let imports = Rc::new(imports);
1134
1135 self.module_directive = directive;
1136 self.export_names = export_names;
1137
1138 self.assert_react_taint_apis(&imports);
1140
1141 if self.is_react_server_layer {
1142 if directive == Some(ModuleDirective::UseClient) {
1143 return;
1144 } else {
1145 self.assert_server_graph(&imports, module);
1151 }
1152 } else {
1153 if directive != Some(ModuleDirective::UseServer)
1158 && directive != Some(ModuleDirective::UseCache)
1159 {
1160 self.assert_client_graph(&imports);
1161 self.assert_invalid_api(module, true);
1162 }
1163 }
1164
1165 module.visit_children_with(self);
1166 }
1167}
1168
1169pub fn server_components_assert(
1176 filename: FileName,
1177 config: Config,
1178 app_dir: Option<PathBuf>,
1179) -> impl Visit {
1180 let is_react_server_layer: bool = match &config {
1181 Config::WithOptions(x) => x.is_react_server_layer,
1182 _ => false,
1183 };
1184 let cache_components_enabled: bool = match &config {
1185 Config::WithOptions(x) => x.cache_components_enabled,
1186 _ => false,
1187 };
1188 let use_cache_enabled: bool = match &config {
1189 Config::WithOptions(x) => x.use_cache_enabled,
1190 _ => false,
1191 };
1192 let taint_enabled: bool = match &config {
1193 Config::WithOptions(x) => x.taint_enabled,
1194 _ => false,
1195 };
1196 let page_extensions: Vec<String> = match &config {
1197 Config::WithOptions(x) => x.page_extensions.clone(),
1198 _ => vec![],
1199 };
1200 let is_app_router: bool = match &config {
1201 Config::WithOptions(x) => x.is_app_router,
1202 _ => false,
1203 };
1204 let filename = match filename {
1205 FileName::Custom(path) => format!("<{path}>"),
1206 _ => filename.to_string(),
1207 };
1208 ReactServerComponentValidator::new(
1209 is_react_server_layer,
1210 cache_components_enabled,
1211 use_cache_enabled,
1212 taint_enabled,
1213 filename,
1214 app_dir,
1215 page_extensions,
1216 is_app_router,
1217 )
1218}
1219
1220pub fn server_components<C: Comments>(
1223 filename: Arc<FileName>,
1224 config: Config,
1225 comments: C,
1226 app_dir: Option<PathBuf>,
1227) -> impl Pass + VisitMut {
1228 let is_react_server_layer: bool = match &config {
1229 Config::WithOptions(x) => x.is_react_server_layer,
1230 _ => false,
1231 };
1232 let cache_components_enabled: bool = match &config {
1233 Config::WithOptions(x) => x.cache_components_enabled,
1234 _ => false,
1235 };
1236 let use_cache_enabled: bool = match &config {
1237 Config::WithOptions(x) => x.use_cache_enabled,
1238 _ => false,
1239 };
1240 let taint_enabled: bool = match &config {
1241 Config::WithOptions(x) => x.taint_enabled,
1242 _ => false,
1243 };
1244 let page_extensions: Vec<String> = match &config {
1245 Config::WithOptions(x) => x.page_extensions.clone(),
1246 _ => vec![],
1247 };
1248 let is_app_router: bool = match &config {
1249 Config::WithOptions(x) => x.is_app_router,
1250 _ => false,
1251 };
1252 visit_mut_pass(ReactServerComponents {
1253 is_react_server_layer,
1254 cache_components_enabled,
1255 use_cache_enabled,
1256 taint_enabled,
1257 comments,
1258 filepath: match &*filename {
1259 FileName::Custom(path) => format!("<{path}>"),
1260 _ => filename.to_string(),
1261 },
1262 app_dir,
1263 page_extensions,
1264 is_app_router,
1265 })
1266}