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