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