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