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 export_names.push(atom!("default"));
570 finished_directives = true;
571 }
572 ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(ExportDefaultExpr { .. })) => {
573 export_names.push(atom!("default"));
574 finished_directives = true;
575 }
576 ModuleItem::ModuleDecl(ModuleDecl::ExportAll(_)) => {
577 export_names.push(atom!("*"));
578 }
579 _ => {
580 finished_directives = true;
581 }
582 }
583 });
584
585 let directive = if is_client_entry {
586 Some(ModuleDirective::UseClient)
587 } else if is_action_file {
588 Some(ModuleDirective::UseServer)
589 } else if is_cache_file {
590 Some(ModuleDirective::UseCache)
591 } else {
592 None
593 };
594
595 (directive, imports, export_names)
596}
597
598struct ReactServerComponentValidator {
600 is_react_server_layer: bool,
601 cache_components_enabled: bool,
602 use_cache_enabled: bool,
603 taint_enabled: bool,
604 filepath: String,
605 app_dir: Option<PathBuf>,
606 invalid_server_imports: Vec<Wtf8Atom>,
607 invalid_server_lib_apis_mapping: FxHashMap<Wtf8Atom, Vec<&'static str>>,
608 deprecated_apis_mapping: FxHashMap<Wtf8Atom, Vec<&'static str>>,
609 invalid_client_imports: Vec<Wtf8Atom>,
610 invalid_client_lib_apis_mapping: FxHashMap<Wtf8Atom, Vec<&'static str>>,
611 react_taint_apis: Vec<&'static str>,
613 pub module_directive: Option<ModuleDirective>,
614 pub export_names: Vec<Atom>,
615 imports: ImportMap,
616 page_extensions: Vec<String>,
617}
618
619impl ReactServerComponentValidator {
620 pub fn new(
621 is_react_server_layer: bool,
622 cache_components_enabled: bool,
623 use_cache_enabled: bool,
624 taint_enabled: bool,
625 filename: String,
626 app_dir: Option<PathBuf>,
627 page_extensions: Vec<String>,
628 ) -> Self {
629 Self {
630 is_react_server_layer,
631 cache_components_enabled,
632 use_cache_enabled,
633 taint_enabled,
634 filepath: filename,
635 app_dir,
636 module_directive: None,
637 export_names: vec![],
638 invalid_server_lib_apis_mapping: FxHashMap::from_iter([
642 (
643 atom!("react").into(),
644 vec![
645 "Component",
646 "createContext",
647 "createFactory",
648 "PureComponent",
649 "useDeferredValue",
650 "useEffect",
651 "useEffectEvent",
652 "useImperativeHandle",
653 "useInsertionEffect",
654 "useLayoutEffect",
655 "useReducer",
656 "useRef",
657 "useState",
658 "useSyncExternalStore",
659 "useTransition",
660 "useOptimistic",
661 "useActionState",
662 "experimental_useOptimistic",
663 ],
664 ),
665 (
666 atom!("react-dom").into(),
667 vec![
668 "flushSync",
669 "unstable_batchedUpdates",
670 "useFormStatus",
671 "useFormState",
672 ],
673 ),
674 (atom!("next/error").into(), vec!["catchError"]),
675 (
676 atom!("next/navigation").into(),
677 vec![
678 "useSearchParams",
679 "usePathname",
680 "useSelectedLayoutSegment",
681 "useSelectedLayoutSegments",
682 "useParams",
683 "useRouter",
684 "useServerInsertedHTML",
685 "ServerInsertedHTMLContext",
686 "unstable_isUnrecognizedActionError",
687 ],
688 ),
689 (atom!("next/link").into(), vec!["useLinkStatus"]),
690 ]),
691 deprecated_apis_mapping: FxHashMap::from_iter([(
692 atom!("next/server").into(),
693 vec!["ImageResponse"],
694 )]),
695
696 invalid_server_imports: vec![
697 atom!("client-only").into(),
698 atom!("react-dom/client").into(),
699 atom!("react-dom/server").into(),
700 atom!("next/router").into(),
701 ],
702
703 invalid_client_imports: vec![
704 atom!("server-only").into(),
705 atom!("next/headers").into(),
706 atom!("next/root-params").into(),
707 ],
708
709 invalid_client_lib_apis_mapping: FxHashMap::from_iter([
710 (atom!("next/server").into(), vec!["after"]),
711 (
712 atom!("next/cache").into(),
713 vec![
714 "revalidatePath",
715 "revalidateTag",
716 "cacheLife",
718 "unstable_cacheLife",
719 "cacheTag",
720 "unstable_cacheTag",
721 ],
723 ),
724 ]),
725 react_taint_apis: vec![
726 "experimental_taintObjectReference",
727 "experimental_taintUniqueValue",
728 ],
729 imports: ImportMap::default(),
730 page_extensions,
731 }
732 }
733
734 fn is_from_node_modules(&self, filepath: &str) -> bool {
735 static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"node_modules[\\/]").unwrap());
736 RE.is_match(filepath)
737 }
738
739 fn is_callee_next_dynamic(&self, callee: &Callee) -> bool {
740 match callee {
741 Callee::Expr(expr) => self.imports.is_import(expr, "next/dynamic", "default"),
742 _ => false,
743 }
744 }
745
746 fn assert_invalid_server_lib_apis(&self, import_source: &Wtf8Atom, import: &ModuleImports) {
751 let deprecated_apis = self.deprecated_apis_mapping.get(import_source);
752 if let Some(deprecated_apis) = deprecated_apis {
753 for specifier in &import.specifiers {
754 if deprecated_apis.contains(&specifier.0.as_str()) {
755 report_error(
756 &self.app_dir,
757 &self.filepath,
758 RSCErrorKind::NextRscErrDeprecatedApi((
759 import_source.to_string_lossy().into_owned(),
760 specifier.0.to_string(),
761 specifier.1,
762 )),
763 );
764 }
765 }
766 }
767
768 let invalid_apis = self.invalid_server_lib_apis_mapping.get(import_source);
769 if let Some(invalid_apis) = invalid_apis {
770 for specifier in &import.specifiers {
771 if invalid_apis.contains(&specifier.0.as_str()) {
772 report_error(
773 &self.app_dir,
774 &self.filepath,
775 RSCErrorKind::NextRscErrReactApi((specifier.0.to_string(), specifier.1)),
776 );
777 }
778 }
779 }
780 }
781
782 fn assert_react_taint_apis(&self, imports: &[ModuleImports]) {
784 if self.taint_enabled || self.is_from_node_modules(&self.filepath) {
786 return;
787 }
788
789 for import in imports {
790 let source = &import.source.0;
791 if source.as_str() != Some("react") {
793 continue;
794 }
795
796 for specifier in &import.specifiers {
797 if self.react_taint_apis.contains(&specifier.0.as_str()) {
798 report_error(
799 &self.app_dir,
800 &self.filepath,
801 RSCErrorKind::NextRscErrTaintWithoutConfig((
802 specifier.0.to_string(),
803 specifier.1,
804 )),
805 );
806 }
807 }
808 }
809 }
810
811 fn assert_server_graph(&self, imports: &[ModuleImports], module: &Module) {
812 if self.is_from_node_modules(&self.filepath) {
814 return;
815 }
816 for import in imports {
817 let source = &import.source.0;
818 if self.invalid_server_imports.contains(source) {
819 report_error(
820 &self.app_dir,
821 &self.filepath,
822 RSCErrorKind::NextRscErrServerImport((
823 source.to_string_lossy().into_owned(),
824 import.source.1,
825 )),
826 );
827 }
828
829 self.assert_invalid_server_lib_apis(source, import);
830 }
831
832 self.assert_invalid_api(module, false);
833 self.assert_server_filename(module);
834 }
835
836 fn assert_server_filename(&self, module: &Module) {
837 if self.is_from_node_modules(&self.filepath) {
838 return;
839 }
840 let ext_pattern = build_page_extensions_regex(&self.page_extensions);
841 let re = Regex::new(&format!(r"[\\/]((global-)?error)\.{ext_pattern}$")).unwrap();
842
843 let is_error_file = re.is_match(&self.filepath);
844
845 if is_error_file
846 && let Some(app_dir) = &self.app_dir
847 && let Some(app_dir) = app_dir.to_str()
848 && self.filepath.starts_with(app_dir)
849 {
850 let span = if let Some(first_item) = module.body.first() {
851 first_item.span()
852 } else {
853 module.span
854 };
855
856 report_error(
857 &self.app_dir,
858 &self.filepath,
859 RSCErrorKind::NextRscErrErrorFileServerComponent(span),
860 );
861 }
862 }
863
864 fn assert_client_graph(&self, imports: &[ModuleImports]) {
865 if self.is_from_node_modules(&self.filepath) {
866 return;
867 }
868 for import in imports {
869 let source = &import.source.0;
870
871 if self.invalid_client_imports.contains(source) {
872 report_error(
873 &self.app_dir,
874 &self.filepath,
875 RSCErrorKind::NextRscErrClientImport((
876 source.to_string_lossy().into_owned(),
877 import.source.1,
878 )),
879 );
880 }
881
882 let invalid_apis = self.invalid_client_lib_apis_mapping.get(source);
883 if let Some(invalid_apis) = invalid_apis {
884 for specifier in &import.specifiers {
885 if invalid_apis.contains(&specifier.0.as_str()) {
886 report_error(
887 &self.app_dir,
888 &self.filepath,
889 RSCErrorKind::NextRscErrClientImport((
890 specifier.0.to_string(),
891 specifier.1,
892 )),
893 );
894 }
895 }
896 }
897 }
898 }
899
900 fn assert_invalid_api(&self, module: &Module, is_client_entry: bool) {
901 if self.is_from_node_modules(&self.filepath) {
902 return;
903 }
904 let ext_pattern = build_page_extensions_regex(&self.page_extensions);
905 let re = Regex::new(&format!(
910 r"[\\/](page|layout|route|icon\d?|apple-icon\d?|opengraph-image\d?|twitter-image\d?|sitemap|robots|manifest)\.{ext_pattern}$",
911 ))
912 .unwrap();
913 let is_app_entry = re.is_match(&self.filepath);
914
915 if is_app_entry {
916 let mut possibly_invalid_exports: FxIndexMap<Atom, (InvalidExportKind, Span)> =
917 FxIndexMap::default();
918
919 let mut collect_possibly_invalid_exports =
920 |export_name: &Atom, span: &Span| match &**export_name {
921 "getServerSideProps" | "getStaticProps" => {
922 possibly_invalid_exports
923 .insert(export_name.clone(), (InvalidExportKind::General, *span));
924 }
925 "generateMetadata" | "metadata" => {
926 possibly_invalid_exports
927 .insert(export_name.clone(), (InvalidExportKind::Metadata, *span));
928 }
929 "runtime" => {
930 if self.cache_components_enabled {
931 possibly_invalid_exports.insert(
932 export_name.clone(),
933 (
934 InvalidExportKind::RouteSegmentConfig(
935 NextConfigProperty::CacheComponents,
936 ),
937 *span,
938 ),
939 );
940 } else if self.use_cache_enabled {
941 possibly_invalid_exports.insert(
942 export_name.clone(),
943 (
944 InvalidExportKind::RouteSegmentConfig(
945 NextConfigProperty::UseCache,
946 ),
947 *span,
948 ),
949 );
950 }
951 }
952 "dynamicParams" | "dynamic" | "fetchCache" | "revalidate"
953 | "experimental_ppr"
954 if self.cache_components_enabled =>
955 {
956 possibly_invalid_exports.insert(
957 export_name.clone(),
958 (
959 InvalidExportKind::RouteSegmentConfig(
960 NextConfigProperty::CacheComponents,
961 ),
962 *span,
963 ),
964 );
965 }
966 "instant" if !self.cache_components_enabled => {
967 possibly_invalid_exports.insert(
968 export_name.clone(),
969 (
970 InvalidExportKind::RequiresRouteSegmentConfig(
971 NextConfigProperty::CacheComponents,
972 ),
973 *span,
974 ),
975 );
976 }
977 _ => (),
978 };
979
980 for export in &module.body {
981 match export {
982 ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(export)) => {
983 for specifier in &export.specifiers {
984 if let ExportSpecifier::Named(named) = specifier {
985 collect_possibly_invalid_exports(&named.orig.atom(), &named.span);
986 }
987 }
988 }
989 ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export)) => match &export.decl {
990 Decl::Fn(f) => {
991 collect_possibly_invalid_exports(&f.ident.sym, &f.ident.span);
992 }
993 Decl::Var(v) => {
994 for decl in &v.decls {
995 if let Pat::Ident(i) = &decl.name {
996 collect_possibly_invalid_exports(&i.sym, &i.span);
997 }
998 }
999 }
1000 _ => {}
1001 },
1002 _ => {}
1003 }
1004 }
1005
1006 for (export_name, (kind, span)) in &possibly_invalid_exports {
1007 match kind {
1008 InvalidExportKind::RouteSegmentConfig(property) => {
1009 report_error(
1010 &self.app_dir,
1011 &self.filepath,
1012 RSCErrorKind::NextRscErrIncompatibleRouteSegmentConfig(
1013 *span,
1014 export_name.to_string(),
1015 *property,
1016 ),
1017 );
1018 }
1019 InvalidExportKind::RequiresRouteSegmentConfig(property) => {
1020 report_error(
1021 &self.app_dir,
1022 &self.filepath,
1023 RSCErrorKind::NextRscErrRequiresRouteSegmentConfig(
1024 *span,
1025 export_name.to_string(),
1026 *property,
1027 ),
1028 );
1029 }
1030 InvalidExportKind::Metadata => {
1031 if is_client_entry
1033 && (export_name == "generateMetadata" || export_name == "metadata")
1034 {
1035 report_error(
1036 &self.app_dir,
1037 &self.filepath,
1038 RSCErrorKind::NextRscErrClientMetadataExport((
1039 export_name.to_string(),
1040 *span,
1041 )),
1042 );
1043 }
1044 }
1047 InvalidExportKind::General => {
1048 report_error(
1049 &self.app_dir,
1050 &self.filepath,
1051 RSCErrorKind::NextRscErrInvalidApi((export_name.to_string(), *span)),
1052 );
1053 }
1054 }
1055 }
1056
1057 if !is_client_entry {
1059 let export1 = possibly_invalid_exports.get(&atom!("generateMetadata"));
1060 let export2 = possibly_invalid_exports.get(&atom!("metadata"));
1061
1062 if let (Some((_, span1)), Some((_, span2))) = (export1, export2) {
1063 report_error(
1064 &self.app_dir,
1065 &self.filepath,
1066 RSCErrorKind::NextRscErrConflictMetadataExport((*span1, *span2)),
1067 );
1068 }
1069 }
1070 }
1071 }
1072
1073 fn check_for_next_ssr_false(&self, node: &CallExpr) -> Option<()> {
1081 if !self.is_callee_next_dynamic(&node.callee) {
1082 return None;
1083 }
1084
1085 let ssr_arg = node.args.get(1)?;
1086 let obj = ssr_arg.expr.as_object()?;
1087
1088 for prop in obj.props.iter().filter_map(|v| v.as_prop()?.as_key_value()) {
1089 let is_ssr = match &prop.key {
1090 PropName::Ident(IdentName { sym, .. }) => sym == "ssr",
1091 PropName::Str(s) => s.value == "ssr",
1092 _ => false,
1093 };
1094
1095 if is_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}