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 "useImperativeHandle",
637 "useInsertionEffect",
638 "useLayoutEffect",
639 "useReducer",
640 "useRef",
641 "useState",
642 "useSyncExternalStore",
643 "useTransition",
644 "useOptimistic",
645 "useActionState",
646 "experimental_useOptimistic",
647 ],
648 ),
649 (
650 atom!("react-dom").into(),
651 vec![
652 "flushSync",
653 "unstable_batchedUpdates",
654 "useFormStatus",
655 "useFormState",
656 ],
657 ),
658 (
659 atom!("next/navigation").into(),
660 vec![
661 "useSearchParams",
662 "usePathname",
663 "useSelectedLayoutSegment",
664 "useSelectedLayoutSegments",
665 "useParams",
666 "useRouter",
667 "useServerInsertedHTML",
668 "ServerInsertedHTMLContext",
669 "unstable_isUnrecognizedActionError",
670 ],
671 ),
672 (atom!("next/link").into(), vec!["useLinkStatus"]),
673 ]),
674 deprecated_apis_mapping: FxHashMap::from_iter([(
675 atom!("next/server").into(),
676 vec!["ImageResponse"],
677 )]),
678
679 invalid_server_imports: vec![
680 atom!("client-only").into(),
681 atom!("react-dom/client").into(),
682 atom!("react-dom/server").into(),
683 atom!("next/router").into(),
684 ],
685
686 invalid_client_imports: vec![
687 atom!("server-only").into(),
688 atom!("next/headers").into(),
689 atom!("next/root-params").into(),
690 ],
691
692 invalid_client_lib_apis_mapping: FxHashMap::from_iter([
693 (atom!("next/server").into(), vec!["after"]),
694 (
695 atom!("next/cache").into(),
696 vec![
697 "revalidatePath",
698 "revalidateTag",
699 "cacheLife",
701 "unstable_cacheLife",
702 "cacheTag",
703 "unstable_cacheTag",
704 ],
706 ),
707 ]),
708 react_taint_apis: vec![
709 "experimental_taintObjectReference",
710 "experimental_taintUniqueValue",
711 ],
712 imports: ImportMap::default(),
713 }
714 }
715
716 fn is_from_node_modules(&self, filepath: &str) -> bool {
717 static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"node_modules[\\/]").unwrap());
718 RE.is_match(filepath)
719 }
720
721 fn is_callee_next_dynamic(&self, callee: &Callee) -> bool {
722 match callee {
723 Callee::Expr(expr) => self.imports.is_import(expr, "next/dynamic", "default"),
724 _ => false,
725 }
726 }
727
728 fn assert_invalid_server_lib_apis(&self, import_source: &Wtf8Atom, import: &ModuleImports) {
733 let deprecated_apis = self.deprecated_apis_mapping.get(import_source);
734 if let Some(deprecated_apis) = deprecated_apis {
735 for specifier in &import.specifiers {
736 if deprecated_apis.contains(&specifier.0.as_str()) {
737 report_error(
738 &self.app_dir,
739 &self.filepath,
740 RSCErrorKind::NextRscErrDeprecatedApi((
741 import_source.to_string_lossy().into_owned(),
742 specifier.0.to_string(),
743 specifier.1,
744 )),
745 );
746 }
747 }
748 }
749
750 let invalid_apis = self.invalid_server_lib_apis_mapping.get(import_source);
751 if let Some(invalid_apis) = invalid_apis {
752 for specifier in &import.specifiers {
753 if invalid_apis.contains(&specifier.0.as_str()) {
754 report_error(
755 &self.app_dir,
756 &self.filepath,
757 RSCErrorKind::NextRscErrReactApi((specifier.0.to_string(), specifier.1)),
758 );
759 }
760 }
761 }
762 }
763
764 fn assert_react_taint_apis(&self, imports: &[ModuleImports]) {
766 if self.taint_enabled || self.is_from_node_modules(&self.filepath) {
768 return;
769 }
770
771 for import in imports {
772 let source = &import.source.0;
773 if source.as_str() != Some("react") {
775 continue;
776 }
777
778 for specifier in &import.specifiers {
779 if self.react_taint_apis.contains(&specifier.0.as_str()) {
780 report_error(
781 &self.app_dir,
782 &self.filepath,
783 RSCErrorKind::NextRscErrTaintWithoutConfig((
784 specifier.0.to_string(),
785 specifier.1,
786 )),
787 );
788 }
789 }
790 }
791 }
792
793 fn assert_server_graph(&self, imports: &[ModuleImports], module: &Module) {
794 if self.is_from_node_modules(&self.filepath) {
796 return;
797 }
798 for import in imports {
799 let source = &import.source.0;
800 if self.invalid_server_imports.contains(source) {
801 report_error(
802 &self.app_dir,
803 &self.filepath,
804 RSCErrorKind::NextRscErrServerImport((
805 source.to_string_lossy().into_owned(),
806 import.source.1,
807 )),
808 );
809 }
810
811 self.assert_invalid_server_lib_apis(source, import);
812 }
813
814 self.assert_invalid_api(module, false);
815 self.assert_server_filename(module);
816 }
817
818 fn assert_server_filename(&self, module: &Module) {
819 if self.is_from_node_modules(&self.filepath) {
820 return;
821 }
822 static RE: Lazy<Regex> =
823 Lazy::new(|| Regex::new(r"[\\/]((global-)?error)\.(ts|js)x?$").unwrap());
824
825 let is_error_file = RE.is_match(&self.filepath);
826
827 if is_error_file {
828 if let Some(app_dir) = &self.app_dir {
829 if let Some(app_dir) = app_dir.to_str() {
830 if self.filepath.starts_with(app_dir) {
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 }
846 }
847
848 fn assert_client_graph(&self, imports: &[ModuleImports]) {
849 if self.is_from_node_modules(&self.filepath) {
850 return;
851 }
852 for import in imports {
853 let source = &import.source.0;
854
855 if self.invalid_client_imports.contains(source) {
856 report_error(
857 &self.app_dir,
858 &self.filepath,
859 RSCErrorKind::NextRscErrClientImport((
860 source.to_string_lossy().into_owned(),
861 import.source.1,
862 )),
863 );
864 }
865
866 let invalid_apis = self.invalid_client_lib_apis_mapping.get(source);
867 if let Some(invalid_apis) = invalid_apis {
868 for specifier in &import.specifiers {
869 if invalid_apis.contains(&specifier.0.as_str()) {
870 report_error(
871 &self.app_dir,
872 &self.filepath,
873 RSCErrorKind::NextRscErrClientImport((
874 specifier.0.to_string(),
875 specifier.1,
876 )),
877 );
878 }
879 }
880 }
881 }
882 }
883
884 fn assert_invalid_api(&self, module: &Module, is_client_entry: bool) {
885 if self.is_from_node_modules(&self.filepath) {
886 return;
887 }
888 static RE: Lazy<Regex> =
889 Lazy::new(|| Regex::new(r"[\\/](page|layout|route)\.(ts|js)x?$").unwrap());
890 let is_app_entry = RE.is_match(&self.filepath);
891
892 if is_app_entry {
893 let mut possibly_invalid_exports: FxIndexMap<Atom, (InvalidExportKind, Span)> =
894 FxIndexMap::default();
895
896 let mut collect_possibly_invalid_exports =
897 |export_name: &Atom, span: &Span| match &**export_name {
898 "getServerSideProps" | "getStaticProps" => {
899 possibly_invalid_exports
900 .insert(export_name.clone(), (InvalidExportKind::General, *span));
901 }
902 "generateMetadata" | "metadata" => {
903 possibly_invalid_exports
904 .insert(export_name.clone(), (InvalidExportKind::Metadata, *span));
905 }
906 "runtime" => {
907 if self.cache_components_enabled {
908 possibly_invalid_exports.insert(
909 export_name.clone(),
910 (
911 InvalidExportKind::RouteSegmentConfig(
912 NextConfigProperty::CacheComponents,
913 ),
914 *span,
915 ),
916 );
917 } else if self.use_cache_enabled {
918 possibly_invalid_exports.insert(
919 export_name.clone(),
920 (
921 InvalidExportKind::RouteSegmentConfig(
922 NextConfigProperty::UseCache,
923 ),
924 *span,
925 ),
926 );
927 }
928 }
929 "dynamicParams" | "dynamic" | "fetchCache" | "revalidate"
930 | "experimental_ppr" => {
931 if self.cache_components_enabled {
932 possibly_invalid_exports.insert(
933 export_name.clone(),
934 (
935 InvalidExportKind::RouteSegmentConfig(
936 NextConfigProperty::CacheComponents,
937 ),
938 *span,
939 ),
940 );
941 }
942 }
943 _ => (),
944 };
945
946 for export in &module.body {
947 match export {
948 ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(export)) => {
949 for specifier in &export.specifiers {
950 if let ExportSpecifier::Named(named) = specifier {
951 collect_possibly_invalid_exports(&named.orig.atom(), &named.span);
952 }
953 }
954 }
955 ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export)) => match &export.decl {
956 Decl::Fn(f) => {
957 collect_possibly_invalid_exports(&f.ident.sym, &f.ident.span);
958 }
959 Decl::Var(v) => {
960 for decl in &v.decls {
961 if let Pat::Ident(i) = &decl.name {
962 collect_possibly_invalid_exports(&i.sym, &i.span);
963 }
964 }
965 }
966 _ => {}
967 },
968 _ => {}
969 }
970 }
971
972 for (export_name, (kind, span)) in &possibly_invalid_exports {
973 match kind {
974 InvalidExportKind::RouteSegmentConfig(property) => {
975 report_error(
976 &self.app_dir,
977 &self.filepath,
978 RSCErrorKind::NextRscErrIncompatibleRouteSegmentConfig(
979 *span,
980 export_name.to_string(),
981 *property,
982 ),
983 );
984 }
985 InvalidExportKind::Metadata => {
986 if is_client_entry
988 && (export_name == "generateMetadata" || export_name == "metadata")
989 {
990 report_error(
991 &self.app_dir,
992 &self.filepath,
993 RSCErrorKind::NextRscErrClientMetadataExport((
994 export_name.to_string(),
995 *span,
996 )),
997 );
998 }
999 }
1002 InvalidExportKind::General => {
1003 report_error(
1004 &self.app_dir,
1005 &self.filepath,
1006 RSCErrorKind::NextRscErrInvalidApi((export_name.to_string(), *span)),
1007 );
1008 }
1009 }
1010 }
1011
1012 if !is_client_entry {
1014 let export1 = possibly_invalid_exports.get(&atom!("generateMetadata"));
1015 let export2 = possibly_invalid_exports.get(&atom!("metadata"));
1016
1017 if let (Some((_, span1)), Some((_, span2))) = (export1, export2) {
1018 report_error(
1019 &self.app_dir,
1020 &self.filepath,
1021 RSCErrorKind::NextRscErrConflictMetadataExport((*span1, *span2)),
1022 );
1023 }
1024 }
1025 }
1026 }
1027
1028 fn check_for_next_ssr_false(&self, node: &CallExpr) -> Option<()> {
1036 if !self.is_callee_next_dynamic(&node.callee) {
1037 return None;
1038 }
1039
1040 let ssr_arg = node.args.get(1)?;
1041 let obj = ssr_arg.expr.as_object()?;
1042
1043 for prop in obj.props.iter().filter_map(|v| v.as_prop()?.as_key_value()) {
1044 let is_ssr = match &prop.key {
1045 PropName::Ident(IdentName { sym, .. }) => sym == "ssr",
1046 PropName::Str(s) => s.value == "ssr",
1047 _ => false,
1048 };
1049
1050 if is_ssr {
1051 let value = prop.value.as_lit()?;
1052 if let Lit::Bool(Bool { value: false, .. }) = value {
1053 report_error(
1054 &self.app_dir,
1055 &self.filepath,
1056 RSCErrorKind::NextSsrDynamicFalseNotAllowed(node.span),
1057 );
1058 }
1059 }
1060 }
1061
1062 None
1063 }
1064}
1065
1066impl Visit for ReactServerComponentValidator {
1067 noop_visit_type!();
1068
1069 fn visit_script(&mut self, script: &swc_core::ecma::ast::Script) {
1072 if script.body.is_empty() {
1073 self.visit_module(&Module::dummy());
1074 }
1075 }
1076
1077 fn visit_call_expr(&mut self, node: &CallExpr) {
1078 node.visit_children_with(self);
1079
1080 if self.is_react_server_layer {
1081 self.check_for_next_ssr_false(node);
1082 }
1083 }
1084
1085 fn visit_module(&mut self, module: &Module) {
1086 self.imports = ImportMap::analyze(module);
1087
1088 let (directive, imports, export_names) =
1089 collect_module_info(&self.app_dir, &self.filepath, module);
1090 let imports = Rc::new(imports);
1091
1092 self.module_directive = directive;
1093 self.export_names = export_names;
1094
1095 self.assert_react_taint_apis(&imports);
1097
1098 if self.is_react_server_layer {
1099 if directive == Some(ModuleDirective::UseClient) {
1100 return;
1101 } else {
1102 self.assert_server_graph(&imports, module);
1108 }
1109 } else {
1110 if directive != Some(ModuleDirective::UseServer)
1115 && directive != Some(ModuleDirective::UseCache)
1116 {
1117 self.assert_client_graph(&imports);
1118 self.assert_invalid_api(module, true);
1119 }
1120 }
1121
1122 module.visit_children_with(self);
1123 }
1124}
1125
1126pub fn server_components_assert(
1133 filename: FileName,
1134 config: Config,
1135 app_dir: Option<PathBuf>,
1136) -> impl Visit {
1137 let is_react_server_layer: bool = match &config {
1138 Config::WithOptions(x) => x.is_react_server_layer,
1139 _ => false,
1140 };
1141 let cache_components_enabled: bool = match &config {
1142 Config::WithOptions(x) => x.cache_components_enabled,
1143 _ => false,
1144 };
1145 let use_cache_enabled: bool = match &config {
1146 Config::WithOptions(x) => x.use_cache_enabled,
1147 _ => false,
1148 };
1149 let taint_enabled: bool = match &config {
1150 Config::WithOptions(x) => x.taint_enabled,
1151 _ => false,
1152 };
1153 let filename = match filename {
1154 FileName::Custom(path) => format!("<{path}>"),
1155 _ => filename.to_string(),
1156 };
1157 ReactServerComponentValidator::new(
1158 is_react_server_layer,
1159 cache_components_enabled,
1160 use_cache_enabled,
1161 taint_enabled,
1162 filename,
1163 app_dir,
1164 )
1165}
1166
1167pub fn server_components<C: Comments>(
1170 filename: Arc<FileName>,
1171 config: Config,
1172 comments: C,
1173 app_dir: Option<PathBuf>,
1174) -> impl Pass + VisitMut {
1175 let is_react_server_layer: bool = match &config {
1176 Config::WithOptions(x) => x.is_react_server_layer,
1177 _ => false,
1178 };
1179 let cache_components_enabled: bool = match &config {
1180 Config::WithOptions(x) => x.cache_components_enabled,
1181 _ => false,
1182 };
1183 let use_cache_enabled: bool = match &config {
1184 Config::WithOptions(x) => x.use_cache_enabled,
1185 _ => false,
1186 };
1187 let taint_enabled: bool = match &config {
1188 Config::WithOptions(x) => x.taint_enabled,
1189 _ => false,
1190 };
1191 visit_mut_pass(ReactServerComponents {
1192 is_react_server_layer,
1193 cache_components_enabled,
1194 use_cache_enabled,
1195 taint_enabled,
1196 comments,
1197 filepath: match &*filename {
1198 FileName::Custom(path) => format!("<{path}>"),
1199 _ => filename.to_string(),
1200 },
1201 app_dir,
1202 })
1203}