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