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, 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 report_error(app_dir: &Option<PathBuf>, filepath: &str, error_kind: RSCErrorKind) {
287 let (msg, spans) = match error_kind {
288 RSCErrorKind::UseClientWithUseServer(span) => (
289 "It's not possible to have both \"use client\" and \"use server\" directives in the \
290 same file."
291 .to_string(),
292 vec![span],
293 ),
294 RSCErrorKind::UseClientWithUseCache(span) => (
295 "It's not possible to have both \"use client\" and \"use cache\" directives in the \
296 same file."
297 .to_string(),
298 vec![span],
299 ),
300 RSCErrorKind::NextRscErrClientDirective(span) => (
301 "The \"use client\" directive must be placed before other expressions. Move it to \
302 the top of the file to resolve this issue."
303 .to_string(),
304 vec![span],
305 ),
306 RSCErrorKind::NextRscErrServerImport((source, span)) => {
307 let msg = match source.as_str() {
308 "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(),
310 "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(),
312 _ => 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")
313 };
314
315 (msg, vec![span])
316 }
317 RSCErrorKind::NextRscErrClientImport((source, span)) => {
318 let is_app_dir = app_dir
319 .as_ref()
320 .map(|app_dir| {
321 if let Some(app_dir) = app_dir.as_os_str().to_str() {
322 filepath.starts_with(app_dir)
323 } else {
324 false
325 }
326 })
327 .unwrap_or_default();
328
329 let msg = if !is_app_dir {
330 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")
331 } else {
332 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")
333 };
334 (msg, vec![span])
335 }
336 RSCErrorKind::NextRscErrReactApi((source, span)) => {
337 let msg = if source == "Component" {
338 "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()
339 } else {
340 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")
341 };
342
343 (msg, vec![span])
344 },
345 RSCErrorKind::NextRscErrErrorFileServerComponent(span) => {
346 (
347 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"),
348 vec![span]
349 )
350 },
351 RSCErrorKind::NextRscErrClientMetadataExport((source, span)) => {
352 (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])
353 },
354 RSCErrorKind::NextRscErrConflictMetadataExport((span1, span2)) => (
355 "\"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(),
356 vec![span1, span2]
357 ),
358 RSCErrorKind::NextRscErrInvalidApi((source, span)) => (
359 format!("\"{source}\" is not supported in app/. Read more: https://nextjs.org/docs/app/building-your-application/data-fetching\n\n"), vec![span]
360 ),
361 RSCErrorKind::NextRscErrDeprecatedApi((source, item, span)) => match (&*source, &*item) {
362 ("next/server", "ImageResponse") => (
363 "ImageResponse moved from \"next/server\" to \"next/og\" since Next.js 14, please \
364 import from \"next/og\" instead"
365 .to_string(),
366 vec![span],
367 ),
368 _ => (format!("\"{source}\" is deprecated."), vec![span]),
369 },
370 RSCErrorKind::NextSsrDynamicFalseNotAllowed(span) => (
371 "`ssr: false` is not allowed with `next/dynamic` in Server Components. Please move it into a Client Component."
372 .to_string(),
373 vec![span],
374 ),
375 RSCErrorKind::NextRscErrIncompatibleRouteSegmentConfig(span, segment, property) => (
376 format!("Route segment config \"{segment}\" is not compatible with `nextConfig.{property}`. Please remove it."),
377 vec![span],
378 ),
379 RSCErrorKind::NextRscErrRequiresRouteSegmentConfig(span, segment, property) => (
380 format!("Route segment config \"{segment}\" requires `nextConfig.{property}` to be enabled."),
381 vec![span],
382 ),
383 RSCErrorKind::NextRscErrTaintWithoutConfig((api_name, span)) => (
384 format!(
385 "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"
386 ),
387 vec![span],
388 ),
389 };
390
391 HANDLER.with(|handler| handler.struct_span_err(spans, msg.as_str()).emit())
392}
393
394fn collect_module_info(
396 app_dir: &Option<PathBuf>,
397 filepath: &str,
398 module: &Module,
399) -> (Option<ModuleDirective>, Vec<ModuleImports>, Vec<Atom>) {
400 let mut imports: Vec<ModuleImports> = vec![];
401 let mut finished_directives = false;
402 let mut is_client_entry = false;
403 let mut is_action_file = false;
404 let mut is_cache_file = false;
405
406 let mut export_names = vec![];
407
408 let _ = &module.body.iter().for_each(|item| {
409 match item {
410 ModuleItem::Stmt(stmt) => {
411 if !stmt.is_expr() {
412 finished_directives = true;
414 }
415
416 match stmt.as_expr() {
417 Some(expr_stmt) => {
418 match &*expr_stmt.expr {
419 Expr::Lit(Lit::Str(Str { value, .. })) => {
420 if &**value == "use client" {
421 if !finished_directives {
422 is_client_entry = true;
423
424 if is_action_file {
425 report_error(
426 app_dir,
427 filepath,
428 RSCErrorKind::UseClientWithUseServer(
429 expr_stmt.span,
430 ),
431 );
432 } else if is_cache_file {
433 report_error(
434 app_dir,
435 filepath,
436 RSCErrorKind::UseClientWithUseCache(expr_stmt.span),
437 );
438 }
439 } else {
440 report_error(
441 app_dir,
442 filepath,
443 RSCErrorKind::NextRscErrClientDirective(expr_stmt.span),
444 );
445 }
446 } else if &**value == "use server" && !finished_directives {
447 is_action_file = true;
448
449 if is_client_entry {
450 report_error(
451 app_dir,
452 filepath,
453 RSCErrorKind::UseClientWithUseServer(expr_stmt.span),
454 );
455 }
456 } else if (&**value == "use cache"
457 || value.starts_with("use cache: "))
458 && !finished_directives
459 {
460 is_cache_file = true;
461
462 if is_client_entry {
463 report_error(
464 app_dir,
465 filepath,
466 RSCErrorKind::UseClientWithUseCache(expr_stmt.span),
467 );
468 }
469 }
470 }
471 Expr::Paren(ParenExpr { expr, .. }) => {
475 finished_directives = true;
476 if let Expr::Lit(Lit::Str(Str { value, .. })) = &**expr
477 && &**value == "use client"
478 {
479 report_error(
480 app_dir,
481 filepath,
482 RSCErrorKind::NextRscErrClientDirective(expr_stmt.span),
483 );
484 }
485 }
486 _ => {
487 finished_directives = true;
489 }
490 }
491 }
492 None => {
493 finished_directives = true;
495 }
496 }
497 }
498 ModuleItem::ModuleDecl(ModuleDecl::Import(
499 import @ ImportDecl {
500 type_only: false, ..
501 },
502 )) => {
503 let source = import.src.value.clone();
504 let specifiers = import
505 .specifiers
506 .iter()
507 .filter(|specifier| {
508 !matches!(
509 specifier,
510 ImportSpecifier::Named(ImportNamedSpecifier {
511 is_type_only: true,
512 ..
513 })
514 )
515 })
516 .map(|specifier| match specifier {
517 ImportSpecifier::Named(named) => match &named.imported {
518 Some(imported) => (imported.atom().into_owned(), imported.span()),
519 None => (named.local.to_id().0, named.local.span),
520 },
521 ImportSpecifier::Default(d) => (atom!(""), d.span),
522 ImportSpecifier::Namespace(n) => (atom!("*"), n.span),
523 })
524 .collect();
525
526 imports.push(ModuleImports {
527 source: (source, import.span),
528 specifiers,
529 });
530
531 finished_directives = true;
532 }
533 ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(e)) => {
535 for specifier in &e.specifiers {
536 export_names.push(match specifier {
537 ExportSpecifier::Default(_) => atom!("default"),
538 ExportSpecifier::Namespace(_) => atom!("*"),
539 ExportSpecifier::Named(named) => named
540 .exported
541 .as_ref()
542 .unwrap_or(&named.orig)
543 .atom()
544 .into_owned(),
545 })
546 }
547 finished_directives = true;
548 }
549 ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl { decl, .. })) => {
550 match decl {
551 Decl::Class(ClassDecl { ident, .. }) => {
552 export_names.push(ident.sym.clone());
553 }
554 Decl::Fn(FnDecl { ident, .. }) => {
555 export_names.push(ident.sym.clone());
556 }
557 Decl::Var(var) => {
558 for decl in &var.decls {
559 if let Pat::Ident(ident) = &decl.name {
560 export_names.push(ident.id.sym.clone());
561 }
562 }
563 }
564 _ => {}
565 }
566 finished_directives = true;
567 }
568 ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(ExportDefaultDecl {
569 decl: _,
570 ..
571 })) => {
572 export_names.push(atom!("default"));
573 finished_directives = true;
574 }
575 ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(ExportDefaultExpr {
576 expr: _,
577 ..
578 })) => {
579 export_names.push(atom!("default"));
580 finished_directives = true;
581 }
582 ModuleItem::ModuleDecl(ModuleDecl::ExportAll(_)) => {
583 export_names.push(atom!("*"));
584 }
585 _ => {
586 finished_directives = true;
587 }
588 }
589 });
590
591 let directive = if is_client_entry {
592 Some(ModuleDirective::UseClient)
593 } else if is_action_file {
594 Some(ModuleDirective::UseServer)
595 } else if is_cache_file {
596 Some(ModuleDirective::UseCache)
597 } else {
598 None
599 };
600
601 (directive, imports, export_names)
602}
603
604struct ReactServerComponentValidator {
606 is_react_server_layer: bool,
607 cache_components_enabled: bool,
608 use_cache_enabled: bool,
609 taint_enabled: bool,
610 filepath: String,
611 app_dir: Option<PathBuf>,
612 invalid_server_imports: Vec<Wtf8Atom>,
613 invalid_server_lib_apis_mapping: FxHashMap<Wtf8Atom, Vec<&'static str>>,
614 deprecated_apis_mapping: FxHashMap<Wtf8Atom, Vec<&'static str>>,
615 invalid_client_imports: Vec<Wtf8Atom>,
616 invalid_client_lib_apis_mapping: FxHashMap<Wtf8Atom, Vec<&'static str>>,
617 react_taint_apis: Vec<&'static str>,
619 pub module_directive: Option<ModuleDirective>,
620 pub export_names: Vec<Atom>,
621 imports: ImportMap,
622 page_extensions: Vec<String>,
623}
624
625impl ReactServerComponentValidator {
626 pub fn new(
627 is_react_server_layer: bool,
628 cache_components_enabled: bool,
629 use_cache_enabled: bool,
630 taint_enabled: bool,
631 filename: String,
632 app_dir: Option<PathBuf>,
633 page_extensions: Vec<String>,
634 ) -> Self {
635 Self {
636 is_react_server_layer,
637 cache_components_enabled,
638 use_cache_enabled,
639 taint_enabled,
640 filepath: filename,
641 app_dir,
642 module_directive: None,
643 export_names: vec![],
644 invalid_server_lib_apis_mapping: FxHashMap::from_iter([
648 (
649 atom!("react").into(),
650 vec![
651 "Component",
652 "createContext",
653 "createFactory",
654 "PureComponent",
655 "useDeferredValue",
656 "useEffect",
657 "useEffectEvent",
658 "useImperativeHandle",
659 "useInsertionEffect",
660 "useLayoutEffect",
661 "useReducer",
662 "useRef",
663 "useState",
664 "useSyncExternalStore",
665 "useTransition",
666 "useOptimistic",
667 "useActionState",
668 "experimental_useOptimistic",
669 ],
670 ),
671 (
672 atom!("react-dom").into(),
673 vec![
674 "flushSync",
675 "unstable_batchedUpdates",
676 "useFormStatus",
677 "useFormState",
678 ],
679 ),
680 (atom!("next/error").into(), vec!["unstable_catchError"]),
681 (
682 atom!("next/navigation").into(),
683 vec![
684 "useSearchParams",
685 "usePathname",
686 "useSelectedLayoutSegment",
687 "useSelectedLayoutSegments",
688 "useParams",
689 "useRouter",
690 "useServerInsertedHTML",
691 "ServerInsertedHTMLContext",
692 "unstable_isUnrecognizedActionError",
693 ],
694 ),
695 (atom!("next/link").into(), vec!["useLinkStatus"]),
696 ]),
697 deprecated_apis_mapping: FxHashMap::from_iter([(
698 atom!("next/server").into(),
699 vec!["ImageResponse"],
700 )]),
701
702 invalid_server_imports: vec![
703 atom!("client-only").into(),
704 atom!("react-dom/client").into(),
705 atom!("react-dom/server").into(),
706 atom!("next/router").into(),
707 ],
708
709 invalid_client_imports: vec![
710 atom!("server-only").into(),
711 atom!("next/headers").into(),
712 atom!("next/root-params").into(),
713 ],
714
715 invalid_client_lib_apis_mapping: FxHashMap::from_iter([
716 (atom!("next/server").into(), vec!["after"]),
717 (
718 atom!("next/cache").into(),
719 vec![
720 "revalidatePath",
721 "revalidateTag",
722 "cacheLife",
724 "unstable_cacheLife",
725 "cacheTag",
726 "unstable_cacheTag",
727 ],
729 ),
730 ]),
731 react_taint_apis: vec![
732 "experimental_taintObjectReference",
733 "experimental_taintUniqueValue",
734 ],
735 imports: ImportMap::default(),
736 page_extensions,
737 }
738 }
739
740 fn is_from_node_modules(&self, filepath: &str) -> bool {
741 static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"node_modules[\\/]").unwrap());
742 RE.is_match(filepath)
743 }
744
745 fn is_callee_next_dynamic(&self, callee: &Callee) -> bool {
746 match callee {
747 Callee::Expr(expr) => self.imports.is_import(expr, "next/dynamic", "default"),
748 _ => false,
749 }
750 }
751
752 fn assert_invalid_server_lib_apis(&self, import_source: &Wtf8Atom, import: &ModuleImports) {
757 let deprecated_apis = self.deprecated_apis_mapping.get(import_source);
758 if let Some(deprecated_apis) = deprecated_apis {
759 for specifier in &import.specifiers {
760 if deprecated_apis.contains(&specifier.0.as_str()) {
761 report_error(
762 &self.app_dir,
763 &self.filepath,
764 RSCErrorKind::NextRscErrDeprecatedApi((
765 import_source.to_string_lossy().into_owned(),
766 specifier.0.to_string(),
767 specifier.1,
768 )),
769 );
770 }
771 }
772 }
773
774 let invalid_apis = self.invalid_server_lib_apis_mapping.get(import_source);
775 if let Some(invalid_apis) = invalid_apis {
776 for specifier in &import.specifiers {
777 if invalid_apis.contains(&specifier.0.as_str()) {
778 report_error(
779 &self.app_dir,
780 &self.filepath,
781 RSCErrorKind::NextRscErrReactApi((specifier.0.to_string(), specifier.1)),
782 );
783 }
784 }
785 }
786 }
787
788 fn assert_react_taint_apis(&self, imports: &[ModuleImports]) {
790 if self.taint_enabled || self.is_from_node_modules(&self.filepath) {
792 return;
793 }
794
795 for import in imports {
796 let source = &import.source.0;
797 if source.as_str() != Some("react") {
799 continue;
800 }
801
802 for specifier in &import.specifiers {
803 if self.react_taint_apis.contains(&specifier.0.as_str()) {
804 report_error(
805 &self.app_dir,
806 &self.filepath,
807 RSCErrorKind::NextRscErrTaintWithoutConfig((
808 specifier.0.to_string(),
809 specifier.1,
810 )),
811 );
812 }
813 }
814 }
815 }
816
817 fn assert_server_graph(&self, imports: &[ModuleImports], module: &Module) {
818 if self.is_from_node_modules(&self.filepath) {
820 return;
821 }
822 for import in imports {
823 let source = &import.source.0;
824 if self.invalid_server_imports.contains(source) {
825 report_error(
826 &self.app_dir,
827 &self.filepath,
828 RSCErrorKind::NextRscErrServerImport((
829 source.to_string_lossy().into_owned(),
830 import.source.1,
831 )),
832 );
833 }
834
835 self.assert_invalid_server_lib_apis(source, import);
836 }
837
838 self.assert_invalid_api(module, false);
839 self.assert_server_filename(module);
840 }
841
842 fn assert_server_filename(&self, module: &Module) {
843 if self.is_from_node_modules(&self.filepath) {
844 return;
845 }
846 let ext_pattern = build_page_extensions_regex(&self.page_extensions);
847 let re = Regex::new(&format!(r"[\\/]((global-)?error)\.{ext_pattern}$")).unwrap();
848
849 let is_error_file = re.is_match(&self.filepath);
850
851 if is_error_file
852 && let Some(app_dir) = &self.app_dir
853 && let Some(app_dir) = app_dir.to_str()
854 && self.filepath.starts_with(app_dir)
855 {
856 let span = if let Some(first_item) = module.body.first() {
857 first_item.span()
858 } else {
859 module.span
860 };
861
862 report_error(
863 &self.app_dir,
864 &self.filepath,
865 RSCErrorKind::NextRscErrErrorFileServerComponent(span),
866 );
867 }
868 }
869
870 fn assert_client_graph(&self, imports: &[ModuleImports]) {
871 if self.is_from_node_modules(&self.filepath) {
872 return;
873 }
874 for import in imports {
875 let source = &import.source.0;
876
877 if self.invalid_client_imports.contains(source) {
878 report_error(
879 &self.app_dir,
880 &self.filepath,
881 RSCErrorKind::NextRscErrClientImport((
882 source.to_string_lossy().into_owned(),
883 import.source.1,
884 )),
885 );
886 }
887
888 let invalid_apis = self.invalid_client_lib_apis_mapping.get(source);
889 if let Some(invalid_apis) = invalid_apis {
890 for specifier in &import.specifiers {
891 if invalid_apis.contains(&specifier.0.as_str()) {
892 report_error(
893 &self.app_dir,
894 &self.filepath,
895 RSCErrorKind::NextRscErrClientImport((
896 specifier.0.to_string(),
897 specifier.1,
898 )),
899 );
900 }
901 }
902 }
903 }
904 }
905
906 fn assert_invalid_api(&self, module: &Module, is_client_entry: bool) {
907 if self.is_from_node_modules(&self.filepath) {
908 return;
909 }
910 let ext_pattern = build_page_extensions_regex(&self.page_extensions);
911 let re = Regex::new(&format!(r"[\\/](page|layout|route)\.{ext_pattern}$")).unwrap();
912 let is_app_entry = re.is_match(&self.filepath);
913
914 if is_app_entry {
915 let mut possibly_invalid_exports: FxIndexMap<Atom, (InvalidExportKind, Span)> =
916 FxIndexMap::default();
917
918 let mut collect_possibly_invalid_exports =
919 |export_name: &Atom, span: &Span| match &**export_name {
920 "getServerSideProps" | "getStaticProps" => {
921 possibly_invalid_exports
922 .insert(export_name.clone(), (InvalidExportKind::General, *span));
923 }
924 "generateMetadata" | "metadata" => {
925 possibly_invalid_exports
926 .insert(export_name.clone(), (InvalidExportKind::Metadata, *span));
927 }
928 "runtime" => {
929 if self.cache_components_enabled {
930 possibly_invalid_exports.insert(
931 export_name.clone(),
932 (
933 InvalidExportKind::RouteSegmentConfig(
934 NextConfigProperty::CacheComponents,
935 ),
936 *span,
937 ),
938 );
939 } else if self.use_cache_enabled {
940 possibly_invalid_exports.insert(
941 export_name.clone(),
942 (
943 InvalidExportKind::RouteSegmentConfig(
944 NextConfigProperty::UseCache,
945 ),
946 *span,
947 ),
948 );
949 }
950 }
951 "dynamicParams" | "dynamic" | "fetchCache" | "revalidate"
952 | "experimental_ppr"
953 if self.cache_components_enabled =>
954 {
955 possibly_invalid_exports.insert(
956 export_name.clone(),
957 (
958 InvalidExportKind::RouteSegmentConfig(
959 NextConfigProperty::CacheComponents,
960 ),
961 *span,
962 ),
963 );
964 }
965 "unstable_instant" if !self.cache_components_enabled => {
966 possibly_invalid_exports.insert(
967 export_name.clone(),
968 (
969 InvalidExportKind::RequiresRouteSegmentConfig(
970 NextConfigProperty::CacheComponents,
971 ),
972 *span,
973 ),
974 );
975 }
976 _ => (),
977 };
978
979 for export in &module.body {
980 match export {
981 ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(export)) => {
982 for specifier in &export.specifiers {
983 if let ExportSpecifier::Named(named) = specifier {
984 collect_possibly_invalid_exports(&named.orig.atom(), &named.span);
985 }
986 }
987 }
988 ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export)) => match &export.decl {
989 Decl::Fn(f) => {
990 collect_possibly_invalid_exports(&f.ident.sym, &f.ident.span);
991 }
992 Decl::Var(v) => {
993 for decl in &v.decls {
994 if let Pat::Ident(i) = &decl.name {
995 collect_possibly_invalid_exports(&i.sym, &i.span);
996 }
997 }
998 }
999 _ => {}
1000 },
1001 _ => {}
1002 }
1003 }
1004
1005 for (export_name, (kind, span)) in &possibly_invalid_exports {
1006 match kind {
1007 InvalidExportKind::RouteSegmentConfig(property) => {
1008 report_error(
1009 &self.app_dir,
1010 &self.filepath,
1011 RSCErrorKind::NextRscErrIncompatibleRouteSegmentConfig(
1012 *span,
1013 export_name.to_string(),
1014 *property,
1015 ),
1016 );
1017 }
1018 InvalidExportKind::RequiresRouteSegmentConfig(property) => {
1019 report_error(
1020 &self.app_dir,
1021 &self.filepath,
1022 RSCErrorKind::NextRscErrRequiresRouteSegmentConfig(
1023 *span,
1024 export_name.to_string(),
1025 *property,
1026 ),
1027 );
1028 }
1029 InvalidExportKind::Metadata => {
1030 if is_client_entry
1032 && (export_name == "generateMetadata" || export_name == "metadata")
1033 {
1034 report_error(
1035 &self.app_dir,
1036 &self.filepath,
1037 RSCErrorKind::NextRscErrClientMetadataExport((
1038 export_name.to_string(),
1039 *span,
1040 )),
1041 );
1042 }
1043 }
1046 InvalidExportKind::General => {
1047 report_error(
1048 &self.app_dir,
1049 &self.filepath,
1050 RSCErrorKind::NextRscErrInvalidApi((export_name.to_string(), *span)),
1051 );
1052 }
1053 }
1054 }
1055
1056 if !is_client_entry {
1058 let export1 = possibly_invalid_exports.get(&atom!("generateMetadata"));
1059 let export2 = possibly_invalid_exports.get(&atom!("metadata"));
1060
1061 if let (Some((_, span1)), Some((_, span2))) = (export1, export2) {
1062 report_error(
1063 &self.app_dir,
1064 &self.filepath,
1065 RSCErrorKind::NextRscErrConflictMetadataExport((*span1, *span2)),
1066 );
1067 }
1068 }
1069 }
1070 }
1071
1072 fn check_for_next_ssr_false(&self, node: &CallExpr) -> Option<()> {
1080 if !self.is_callee_next_dynamic(&node.callee) {
1081 return None;
1082 }
1083
1084 let ssr_arg = node.args.get(1)?;
1085 let obj = ssr_arg.expr.as_object()?;
1086
1087 for prop in obj.props.iter().filter_map(|v| v.as_prop()?.as_key_value()) {
1088 let is_ssr = match &prop.key {
1089 PropName::Ident(IdentName { sym, .. }) => sym == "ssr",
1090 PropName::Str(s) => s.value == "ssr",
1091 _ => false,
1092 };
1093
1094 if is_ssr {
1095 let value = prop.value.as_lit()?;
1096 if let Lit::Bool(Bool { value: false, .. }) = value {
1097 report_error(
1098 &self.app_dir,
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) =
1133 collect_module_info(&self.app_dir, &self.filepath, module);
1134 let imports = Rc::new(imports);
1135
1136 self.module_directive = directive;
1137 self.export_names = export_names;
1138
1139 self.assert_react_taint_apis(&imports);
1141
1142 if self.is_react_server_layer {
1143 if directive == Some(ModuleDirective::UseClient) {
1144 return;
1145 } else {
1146 self.assert_server_graph(&imports, module);
1152 }
1153 } else {
1154 if directive != Some(ModuleDirective::UseServer)
1159 && directive != Some(ModuleDirective::UseCache)
1160 {
1161 self.assert_client_graph(&imports);
1162 self.assert_invalid_api(module, true);
1163 }
1164 }
1165
1166 module.visit_children_with(self);
1167 }
1168}
1169
1170pub fn server_components_assert(
1177 filename: FileName,
1178 config: Config,
1179 app_dir: Option<PathBuf>,
1180) -> impl Visit {
1181 let is_react_server_layer: bool = match &config {
1182 Config::WithOptions(x) => x.is_react_server_layer,
1183 _ => false,
1184 };
1185 let cache_components_enabled: bool = match &config {
1186 Config::WithOptions(x) => x.cache_components_enabled,
1187 _ => false,
1188 };
1189 let use_cache_enabled: bool = match &config {
1190 Config::WithOptions(x) => x.use_cache_enabled,
1191 _ => false,
1192 };
1193 let taint_enabled: bool = match &config {
1194 Config::WithOptions(x) => x.taint_enabled,
1195 _ => false,
1196 };
1197 let page_extensions: Vec<String> = match &config {
1198 Config::WithOptions(x) => x.page_extensions.clone(),
1199 _ => vec![],
1200 };
1201 let filename = match filename {
1202 FileName::Custom(path) => format!("<{path}>"),
1203 _ => filename.to_string(),
1204 };
1205 ReactServerComponentValidator::new(
1206 is_react_server_layer,
1207 cache_components_enabled,
1208 use_cache_enabled,
1209 taint_enabled,
1210 filename,
1211 app_dir,
1212 page_extensions,
1213 )
1214}
1215
1216pub fn server_components<C: Comments>(
1219 filename: Arc<FileName>,
1220 config: Config,
1221 comments: C,
1222 app_dir: Option<PathBuf>,
1223) -> impl Pass + VisitMut {
1224 let is_react_server_layer: bool = match &config {
1225 Config::WithOptions(x) => x.is_react_server_layer,
1226 _ => false,
1227 };
1228 let cache_components_enabled: bool = match &config {
1229 Config::WithOptions(x) => x.cache_components_enabled,
1230 _ => false,
1231 };
1232 let use_cache_enabled: bool = match &config {
1233 Config::WithOptions(x) => x.use_cache_enabled,
1234 _ => false,
1235 };
1236 let taint_enabled: bool = match &config {
1237 Config::WithOptions(x) => x.taint_enabled,
1238 _ => false,
1239 };
1240 let page_extensions: Vec<String> = match &config {
1241 Config::WithOptions(x) => x.page_extensions.clone(),
1242 _ => vec![],
1243 };
1244 visit_mut_pass(ReactServerComponents {
1245 is_react_server_layer,
1246 cache_components_enabled,
1247 use_cache_enabled,
1248 taint_enabled,
1249 comments,
1250 filepath: match &*filename {
1251 FileName::Custom(path) => format!("<{path}>"),
1252 _ => filename.to_string(),
1253 },
1254 app_dir,
1255 page_extensions,
1256 })
1257}