next_custom_transforms/transforms/
react_server_components.rs

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},
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}
57
58/// A visitor that transforms given module to use module proxy if it's a React
59/// server component.
60/// **NOTE** Turbopack uses ClientDirectiveTransformer for the
61/// same purpose, so does not run this transform.
62struct ReactServerComponents<C: Comments> {
63    is_react_server_layer: bool,
64    cache_components_enabled: bool,
65    use_cache_enabled: bool,
66    filepath: String,
67    app_dir: Option<PathBuf>,
68    comments: C,
69    directive_import_collection: Option<(bool, bool, RcVec<ModuleImports>, RcVec<Atom>)>,
70}
71
72#[derive(Clone, Debug)]
73struct ModuleImports {
74    source: (Atom, Span),
75    specifiers: Vec<(Atom, Span)>,
76}
77
78enum RSCErrorKind {
79    /// When `use client` and `use server` are in the same file.
80    /// It's not possible to have both directives in the same file.
81    RedundantDirectives(Span),
82    NextRscErrServerImport((String, Span)),
83    NextRscErrClientImport((String, Span)),
84    NextRscErrClientDirective(Span),
85    NextRscErrReactApi((String, Span)),
86    NextRscErrErrorFileServerComponent(Span),
87    NextRscErrClientMetadataExport((String, Span)),
88    NextRscErrConflictMetadataExport((Span, Span)),
89    NextRscErrInvalidApi((String, Span)),
90    NextRscErrDeprecatedApi((String, String, Span)),
91    NextSsrDynamicFalseNotAllowed(Span),
92    NextRscErrIncompatibleRouteSegmentConfig(Span, String, NextConfigProperty),
93}
94
95#[derive(Clone, Debug, Copy)]
96enum NextConfigProperty {
97    CacheComponents,
98    UseCache,
99}
100
101impl Display for NextConfigProperty {
102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103        match self {
104            NextConfigProperty::CacheComponents => write!(f, "experimental.cacheComponents"),
105            NextConfigProperty::UseCache => write!(f, "experimental.useCache"),
106        }
107    }
108}
109
110enum InvalidExportKind {
111    General,
112    Metadata,
113    RouteSegmentConfig(NextConfigProperty),
114}
115
116impl<C: Comments> VisitMut for ReactServerComponents<C> {
117    noop_visit_mut_type!();
118
119    fn visit_mut_module(&mut self, module: &mut Module) {
120        // Run the validator first to assert, collect directives and imports.
121        let mut validator = ReactServerComponentValidator::new(
122            self.is_react_server_layer,
123            self.cache_components_enabled,
124            self.use_cache_enabled,
125            self.filepath.clone(),
126            self.app_dir.clone(),
127        );
128
129        module.visit_with(&mut validator);
130        self.directive_import_collection = validator.directive_import_collection;
131
132        let is_client_entry = self
133            .directive_import_collection
134            .as_ref()
135            .expect("directive_import_collection must be set")
136            .0;
137
138        self.remove_top_level_directive(module);
139
140        let is_cjs = contains_cjs(module);
141
142        if self.is_react_server_layer {
143            if is_client_entry {
144                self.to_module_ref(module, is_cjs);
145                return;
146            }
147        } else if is_client_entry {
148            self.prepend_comment_node(module, is_cjs);
149        }
150        module.visit_mut_children_with(self)
151    }
152}
153
154impl<C: Comments> ReactServerComponents<C> {
155    /// removes specific directive from the AST.
156    fn remove_top_level_directive(&mut self, module: &mut Module) {
157        module.body.retain(|item| {
158            if let ModuleItem::Stmt(stmt) = item {
159                if let Some(expr_stmt) = stmt.as_expr() {
160                    if let Expr::Lit(Lit::Str(Str { value, .. })) = &*expr_stmt.expr {
161                        if &**value == "use client" {
162                            // Remove the directive.
163                            return false;
164                        }
165                    }
166                }
167            }
168            true
169        });
170    }
171
172    // Convert the client module to the module reference code and add a special
173    // comment to the top of the file.
174    fn to_module_ref(&self, module: &mut Module, is_cjs: bool) {
175        // Clear all the statements and module declarations.
176        module.body.clear();
177
178        let proxy_ident = quote_ident!("createProxy");
179        let filepath = quote_str!(&*self.filepath);
180
181        prepend_stmts(
182            &mut module.body,
183            vec![
184                ModuleItem::Stmt(Stmt::Decl(Decl::Var(Box::new(VarDecl {
185                    span: DUMMY_SP,
186                    kind: VarDeclKind::Const,
187                    decls: vec![VarDeclarator {
188                        span: DUMMY_SP,
189                        name: Pat::Object(ObjectPat {
190                            span: DUMMY_SP,
191                            props: vec![ObjectPatProp::Assign(AssignPatProp {
192                                span: DUMMY_SP,
193                                key: proxy_ident.into(),
194                                value: None,
195                            })],
196                            optional: false,
197                            type_ann: None,
198                        }),
199                        init: Some(Box::new(Expr::Call(CallExpr {
200                            span: DUMMY_SP,
201                            callee: quote_ident!("require").as_callee(),
202                            args: vec![quote_str!("private-next-rsc-mod-ref-proxy").as_arg()],
203                            ..Default::default()
204                        }))),
205                        definite: false,
206                    }],
207                    ..Default::default()
208                })))),
209                ModuleItem::Stmt(Stmt::Expr(ExprStmt {
210                    span: DUMMY_SP,
211                    expr: Box::new(Expr::Assign(AssignExpr {
212                        span: DUMMY_SP,
213                        left: MemberExpr {
214                            span: DUMMY_SP,
215                            obj: Box::new(Expr::Ident(quote_ident!("module").into())),
216                            prop: MemberProp::Ident(quote_ident!("exports")),
217                        }
218                        .into(),
219                        op: op!("="),
220                        right: Box::new(Expr::Call(CallExpr {
221                            span: DUMMY_SP,
222                            callee: quote_ident!("createProxy").as_callee(),
223                            args: vec![filepath.as_arg()],
224                            ..Default::default()
225                        })),
226                    })),
227                })),
228            ]
229            .into_iter(),
230        );
231
232        self.prepend_comment_node(module, is_cjs);
233    }
234
235    fn prepend_comment_node(&self, module: &Module, is_cjs: bool) {
236        let export_names = &self
237            .directive_import_collection
238            .as_ref()
239            .expect("directive_import_collection must be set")
240            .3;
241
242        // Prepend a special comment to the top of the file that contains
243        // module export names and the detected module type.
244        self.comments.add_leading(
245            module.span.lo,
246            Comment {
247                span: DUMMY_SP,
248                kind: CommentKind::Block,
249                text: format!(
250                    " __next_internal_client_entry_do_not_use__ {} {} ",
251                    join_atoms(export_names),
252                    if is_cjs { "cjs" } else { "auto" }
253                )
254                .into(),
255            },
256        );
257    }
258}
259
260fn join_atoms(atoms: &[Atom]) -> String {
261    atoms
262        .iter()
263        .map(|atom| atom.as_ref())
264        .collect::<Vec<_>>()
265        .join(",")
266}
267
268/// Consolidated place to parse, generate error messages for the RSC parsing
269/// errors.
270fn report_error(app_dir: &Option<PathBuf>, filepath: &str, error_kind: RSCErrorKind) {
271    let (msg, spans) = match error_kind {
272        RSCErrorKind::RedundantDirectives(span) => (
273            "It's not possible to have both `use client` and `use server` directives in the \
274             same file."
275                .to_string(),
276            vec![span],
277        ),
278        RSCErrorKind::NextRscErrClientDirective(span) => (
279            "The \"use client\" directive must be placed before other expressions. Move it to \
280             the top of the file to resolve this issue."
281                .to_string(),
282            vec![span],
283        ),
284        RSCErrorKind::NextRscErrServerImport((source, span)) => {
285            let msg = match source.as_str() {
286                // If importing "react-dom/server", we should show a different error.
287                "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(),
288                // If importing "next/router", we should tell them to use "next/navigation".
289                "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(),
290                _ => 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")
291            };
292
293            (msg, vec![span])
294        }
295        RSCErrorKind::NextRscErrClientImport((source, span)) => {
296            let is_app_dir = app_dir
297                .as_ref()
298                .map(|app_dir| {
299                    if let Some(app_dir) = app_dir.as_os_str().to_str() {
300                        filepath.starts_with(app_dir)
301                    } else {
302                        false
303                    }
304                })
305                .unwrap_or_default();
306
307            let msg = if !is_app_dir {
308                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")
309            } else {
310                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")
311            };
312            (msg, vec![span])
313        }
314        RSCErrorKind::NextRscErrReactApi((source, span)) => {
315            let msg = if source == "Component" {
316                "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()
317            } else {
318                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")
319            };
320
321            (msg, vec![span])
322        },
323        RSCErrorKind::NextRscErrErrorFileServerComponent(span) => {
324            (
325                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"),
326                vec![span]
327            )
328        },
329        RSCErrorKind::NextRscErrClientMetadataExport((source, span)) => {
330            (format!("You are attempting to export \"{source}\" from a component marked with \"use client\", which is disallowed. Either remove the export, or the \"use client\" directive. Read more: https://nextjs.org/docs/app/api-reference/directives/use-client\n\n"), vec![span])
331        },
332        RSCErrorKind::NextRscErrConflictMetadataExport((span1, span2)) => (
333            "\"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(),
334            vec![span1, span2]
335        ),
336        RSCErrorKind::NextRscErrInvalidApi((source, span)) => (
337            format!("\"{source}\" is not supported in app/. Read more: https://nextjs.org/docs/app/building-your-application/data-fetching\n\n"), vec![span]
338        ),
339        RSCErrorKind::NextRscErrDeprecatedApi((source, item, span)) => match (&*source, &*item) {
340            ("next/server", "ImageResponse") => (
341                "ImageResponse moved from \"next/server\" to \"next/og\" since Next.js 14, please \
342                 import from \"next/og\" instead"
343                    .to_string(),
344                vec![span],
345            ),
346            _ => (format!("\"{source}\" is deprecated."), vec![span]),
347        },
348        RSCErrorKind::NextSsrDynamicFalseNotAllowed(span) => (
349            "`ssr: false` is not allowed with `next/dynamic` in Server Components. Please move it into a Client Component."
350                .to_string(),
351            vec![span],
352        ),
353        RSCErrorKind::NextRscErrIncompatibleRouteSegmentConfig(span, segment, property) => (
354            format!("Route segment config \"{segment}\" is not compatible with `nextConfig.{property}`. Please remove it."),
355            vec![span],
356        ),
357    };
358
359    HANDLER.with(|handler| handler.struct_span_err(spans, msg.as_str()).emit())
360}
361
362/// Collects top level directives and imports
363fn collect_top_level_directives_and_imports(
364    app_dir: &Option<PathBuf>,
365    filepath: &str,
366    module: &Module,
367) -> (bool, bool, Vec<ModuleImports>, Vec<Atom>) {
368    let mut imports: Vec<ModuleImports> = vec![];
369    let mut finished_directives = false;
370    let mut is_client_entry = false;
371    let mut is_action_file = false;
372
373    let mut export_names = vec![];
374
375    let _ = &module.body.iter().for_each(|item| {
376        match item {
377            ModuleItem::Stmt(stmt) => {
378                if !stmt.is_expr() {
379                    // Not an expression.
380                    finished_directives = true;
381                }
382
383                match stmt.as_expr() {
384                    Some(expr_stmt) => {
385                        match &*expr_stmt.expr {
386                            Expr::Lit(Lit::Str(Str { value, .. })) => {
387                                if &**value == "use client" {
388                                    if !finished_directives {
389                                        is_client_entry = true;
390
391                                        if is_action_file {
392                                            report_error(
393                                                app_dir,
394                                                filepath,
395                                                RSCErrorKind::RedundantDirectives(expr_stmt.span),
396                                            );
397                                        }
398                                    } else {
399                                        report_error(
400                                            app_dir,
401                                            filepath,
402                                            RSCErrorKind::NextRscErrClientDirective(expr_stmt.span),
403                                        );
404                                    }
405                                } else if &**value == "use server" && !finished_directives {
406                                    is_action_file = true;
407
408                                    if is_client_entry {
409                                        report_error(
410                                            app_dir,
411                                            filepath,
412                                            RSCErrorKind::RedundantDirectives(expr_stmt.span),
413                                        );
414                                    }
415                                }
416                            }
417                            // Match `ParenthesisExpression` which is some formatting tools
418                            // usually do: ('use client'). In these case we need to throw
419                            // an exception because they are not valid directives.
420                            Expr::Paren(ParenExpr { expr, .. }) => {
421                                finished_directives = true;
422                                if let Expr::Lit(Lit::Str(Str { value, .. })) = &**expr {
423                                    if &**value == "use client" {
424                                        report_error(
425                                            app_dir,
426                                            filepath,
427                                            RSCErrorKind::NextRscErrClientDirective(expr_stmt.span),
428                                        );
429                                    }
430                                }
431                            }
432                            _ => {
433                                // Other expression types.
434                                finished_directives = true;
435                            }
436                        }
437                    }
438                    None => {
439                        // Not an expression.
440                        finished_directives = true;
441                    }
442                }
443            }
444            ModuleItem::ModuleDecl(ModuleDecl::Import(
445                import @ ImportDecl {
446                    type_only: false, ..
447                },
448            )) => {
449                let source = import.src.value.clone();
450                let specifiers = import
451                    .specifiers
452                    .iter()
453                    .filter(|specifier| {
454                        !matches!(
455                            specifier,
456                            ImportSpecifier::Named(ImportNamedSpecifier {
457                                is_type_only: true,
458                                ..
459                            })
460                        )
461                    })
462                    .map(|specifier| match specifier {
463                        ImportSpecifier::Named(named) => match &named.imported {
464                            Some(imported) => match &imported {
465                                ModuleExportName::Ident(i) => (i.to_id().0, i.span),
466                                ModuleExportName::Str(s) => (s.value.clone(), s.span),
467                            },
468                            None => (named.local.to_id().0, named.local.span),
469                        },
470                        ImportSpecifier::Default(d) => (atom!(""), d.span),
471                        ImportSpecifier::Namespace(n) => (atom!("*"), n.span),
472                    })
473                    .collect();
474
475                imports.push(ModuleImports {
476                    source: (source, import.span),
477                    specifiers,
478                });
479
480                finished_directives = true;
481            }
482            // Collect all export names.
483            ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(e)) => {
484                for specifier in &e.specifiers {
485                    export_names.push(match specifier {
486                        ExportSpecifier::Default(_) => atom!("default"),
487                        ExportSpecifier::Namespace(_) => atom!("*"),
488                        ExportSpecifier::Named(named) => match &named.exported {
489                            Some(exported) => match &exported {
490                                ModuleExportName::Ident(i) => i.sym.clone(),
491                                ModuleExportName::Str(s) => s.value.clone(),
492                            },
493                            _ => match &named.orig {
494                                ModuleExportName::Ident(i) => i.sym.clone(),
495                                ModuleExportName::Str(s) => s.value.clone(),
496                            },
497                        },
498                    })
499                }
500                finished_directives = true;
501            }
502            ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl { decl, .. })) => {
503                match decl {
504                    Decl::Class(ClassDecl { ident, .. }) => {
505                        export_names.push(ident.sym.clone());
506                    }
507                    Decl::Fn(FnDecl { ident, .. }) => {
508                        export_names.push(ident.sym.clone());
509                    }
510                    Decl::Var(var) => {
511                        for decl in &var.decls {
512                            if let Pat::Ident(ident) = &decl.name {
513                                export_names.push(ident.id.sym.clone());
514                            }
515                        }
516                    }
517                    _ => {}
518                }
519                finished_directives = true;
520            }
521            ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(ExportDefaultDecl {
522                decl: _,
523                ..
524            })) => {
525                export_names.push(atom!("default"));
526                finished_directives = true;
527            }
528            ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(ExportDefaultExpr {
529                expr: _,
530                ..
531            })) => {
532                export_names.push(atom!("default"));
533                finished_directives = true;
534            }
535            ModuleItem::ModuleDecl(ModuleDecl::ExportAll(_)) => {
536                export_names.push(atom!("*"));
537            }
538            _ => {
539                finished_directives = true;
540            }
541        }
542    });
543
544    (is_client_entry, is_action_file, imports, export_names)
545}
546
547/// A visitor to assert given module file is a valid React server component.
548struct ReactServerComponentValidator {
549    is_react_server_layer: bool,
550    cache_components_enabled: bool,
551    use_cache_enabled: bool,
552    filepath: String,
553    app_dir: Option<PathBuf>,
554    invalid_server_imports: Vec<Atom>,
555    invalid_server_lib_apis_mapping: FxHashMap<&'static str, Vec<&'static str>>,
556    deprecated_apis_mapping: FxHashMap<&'static str, Vec<&'static str>>,
557    invalid_client_imports: Vec<Atom>,
558    invalid_client_lib_apis_mapping: FxHashMap<&'static str, Vec<&'static str>>,
559    pub directive_import_collection: Option<(bool, bool, RcVec<ModuleImports>, RcVec<Atom>)>,
560    imports: ImportMap,
561}
562
563// A type to workaround a clippy warning.
564type RcVec<T> = Rc<Vec<T>>;
565
566impl ReactServerComponentValidator {
567    pub fn new(
568        is_react_server_layer: bool,
569        cache_components_enabled: bool,
570        use_cache_enabled: bool,
571        filename: String,
572        app_dir: Option<PathBuf>,
573    ) -> Self {
574        Self {
575            is_react_server_layer,
576            cache_components_enabled,
577            use_cache_enabled,
578            filepath: filename,
579            app_dir,
580            directive_import_collection: None,
581            // react -> [apis]
582            // react-dom -> [apis]
583            // next/navigation -> [apis]
584            invalid_server_lib_apis_mapping: FxHashMap::from_iter([
585                (
586                    "react",
587                    vec![
588                        "Component",
589                        "createContext",
590                        "createFactory",
591                        "PureComponent",
592                        "useDeferredValue",
593                        "useEffect",
594                        "useImperativeHandle",
595                        "useInsertionEffect",
596                        "useLayoutEffect",
597                        "useReducer",
598                        "useRef",
599                        "useState",
600                        "useSyncExternalStore",
601                        "useTransition",
602                        "useOptimistic",
603                        "useActionState",
604                        "experimental_useOptimistic",
605                    ],
606                ),
607                (
608                    "react-dom",
609                    vec![
610                        "flushSync",
611                        "unstable_batchedUpdates",
612                        "useFormStatus",
613                        "useFormState",
614                    ],
615                ),
616                (
617                    "next/navigation",
618                    vec![
619                        "useSearchParams",
620                        "usePathname",
621                        "useSelectedLayoutSegment",
622                        "useSelectedLayoutSegments",
623                        "useParams",
624                        "useRouter",
625                        "useServerInsertedHTML",
626                        "ServerInsertedHTMLContext",
627                        "unstable_isUnrecognizedActionError",
628                    ],
629                ),
630                ("next/link", vec!["useLinkStatus"]),
631            ]),
632            deprecated_apis_mapping: FxHashMap::from_iter([("next/server", vec!["ImageResponse"])]),
633
634            invalid_server_imports: vec![
635                Atom::from("client-only"),
636                Atom::from("react-dom/client"),
637                Atom::from("react-dom/server"),
638                Atom::from("next/router"),
639            ],
640
641            invalid_client_imports: vec![
642                Atom::from("server-only"),
643                Atom::from("next/headers"),
644                Atom::from("next/root-params"),
645            ],
646
647            invalid_client_lib_apis_mapping: FxHashMap::from_iter([
648                ("next/server", vec!["after"]),
649                (
650                    "next/cache",
651                    vec![
652                        "revalidatePath",
653                        "revalidateTag",
654                        // "unstable_cache", // useless in client, but doesn't technically error
655                        "unstable_cacheLife",
656                        "unstable_cacheTag",
657                        // "unstable_noStore" // no-op in client, but allowed for legacy reasons
658                    ],
659                ),
660            ]),
661            imports: ImportMap::default(),
662        }
663    }
664
665    fn is_from_node_modules(&self, filepath: &str) -> bool {
666        static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"node_modules[\\/]").unwrap());
667        RE.is_match(filepath)
668    }
669
670    fn is_callee_next_dynamic(&self, callee: &Callee) -> bool {
671        match callee {
672            Callee::Expr(expr) => self.imports.is_import(expr, "next/dynamic", "default"),
673            _ => false,
674        }
675    }
676
677    // Asserts the server lib apis
678    // e.g.
679    // assert_invalid_server_lib_apis("react", import)
680    // assert_invalid_server_lib_apis("react-dom", import)
681    fn assert_invalid_server_lib_apis(&self, import_source: String, import: &ModuleImports) {
682        let deprecated_apis = self.deprecated_apis_mapping.get(import_source.as_str());
683        if let Some(deprecated_apis) = deprecated_apis {
684            for specifier in &import.specifiers {
685                if deprecated_apis.contains(&specifier.0.as_str()) {
686                    report_error(
687                        &self.app_dir,
688                        &self.filepath,
689                        RSCErrorKind::NextRscErrDeprecatedApi((
690                            import_source.clone(),
691                            specifier.0.to_string(),
692                            specifier.1,
693                        )),
694                    );
695                }
696            }
697        }
698
699        let invalid_apis = self
700            .invalid_server_lib_apis_mapping
701            .get(import_source.as_str());
702        if let Some(invalid_apis) = invalid_apis {
703            for specifier in &import.specifiers {
704                if invalid_apis.contains(&specifier.0.as_str()) {
705                    report_error(
706                        &self.app_dir,
707                        &self.filepath,
708                        RSCErrorKind::NextRscErrReactApi((specifier.0.to_string(), specifier.1)),
709                    );
710                }
711            }
712        }
713    }
714
715    fn assert_server_graph(&self, imports: &[ModuleImports], module: &Module) {
716        // If the
717        if self.is_from_node_modules(&self.filepath) {
718            return;
719        }
720        for import in imports {
721            let source = import.source.0.clone();
722            let source_str = source.to_string();
723            if self.invalid_server_imports.contains(&source) {
724                report_error(
725                    &self.app_dir,
726                    &self.filepath,
727                    RSCErrorKind::NextRscErrServerImport((source_str.clone(), import.source.1)),
728                );
729            }
730
731            self.assert_invalid_server_lib_apis(source_str, import);
732        }
733
734        self.assert_invalid_api(module, false);
735        self.assert_server_filename(module);
736    }
737
738    fn assert_server_filename(&self, module: &Module) {
739        if self.is_from_node_modules(&self.filepath) {
740            return;
741        }
742        static RE: Lazy<Regex> =
743            Lazy::new(|| Regex::new(r"[\\/]((global-)?error)\.(ts|js)x?$").unwrap());
744
745        let is_error_file = RE.is_match(&self.filepath);
746
747        if is_error_file {
748            if let Some(app_dir) = &self.app_dir {
749                if let Some(app_dir) = app_dir.to_str() {
750                    if self.filepath.starts_with(app_dir) {
751                        let span = if let Some(first_item) = module.body.first() {
752                            first_item.span()
753                        } else {
754                            module.span
755                        };
756
757                        report_error(
758                            &self.app_dir,
759                            &self.filepath,
760                            RSCErrorKind::NextRscErrErrorFileServerComponent(span),
761                        );
762                    }
763                }
764            }
765        }
766    }
767
768    fn assert_client_graph(&self, imports: &[ModuleImports]) {
769        if self.is_from_node_modules(&self.filepath) {
770            return;
771        }
772        for import in imports {
773            let source = &import.source.0;
774
775            if self.invalid_client_imports.contains(source) {
776                report_error(
777                    &self.app_dir,
778                    &self.filepath,
779                    RSCErrorKind::NextRscErrClientImport((source.to_string(), import.source.1)),
780                );
781            }
782
783            let invalid_apis = self.invalid_client_lib_apis_mapping.get(source.as_str());
784            if let Some(invalid_apis) = invalid_apis {
785                for specifier in &import.specifiers {
786                    if invalid_apis.contains(&specifier.0.as_str()) {
787                        report_error(
788                            &self.app_dir,
789                            &self.filepath,
790                            RSCErrorKind::NextRscErrClientImport((
791                                specifier.0.to_string(),
792                                specifier.1,
793                            )),
794                        );
795                    }
796                }
797            }
798        }
799    }
800
801    fn assert_invalid_api(&self, module: &Module, is_client_entry: bool) {
802        if self.is_from_node_modules(&self.filepath) {
803            return;
804        }
805        static RE: Lazy<Regex> =
806            Lazy::new(|| Regex::new(r"[\\/](page|layout|route)\.(ts|js)x?$").unwrap());
807        let is_app_entry = RE.is_match(&self.filepath);
808
809        if is_app_entry {
810            let mut possibly_invalid_exports: FxIndexMap<Atom, (InvalidExportKind, Span)> =
811                FxIndexMap::default();
812
813            let mut collect_possibly_invalid_exports =
814                |export_name: &Atom, span: &Span| match &**export_name {
815                    "getServerSideProps" | "getStaticProps" => {
816                        possibly_invalid_exports
817                            .insert(export_name.clone(), (InvalidExportKind::General, *span));
818                    }
819                    "generateMetadata" | "metadata" => {
820                        possibly_invalid_exports
821                            .insert(export_name.clone(), (InvalidExportKind::Metadata, *span));
822                    }
823                    "runtime" => {
824                        if self.cache_components_enabled {
825                            possibly_invalid_exports.insert(
826                                export_name.clone(),
827                                (
828                                    InvalidExportKind::RouteSegmentConfig(
829                                        NextConfigProperty::CacheComponents,
830                                    ),
831                                    *span,
832                                ),
833                            );
834                        } else if self.use_cache_enabled {
835                            possibly_invalid_exports.insert(
836                                export_name.clone(),
837                                (
838                                    InvalidExportKind::RouteSegmentConfig(
839                                        NextConfigProperty::UseCache,
840                                    ),
841                                    *span,
842                                ),
843                            );
844                        }
845                    }
846                    "dynamicParams" | "dynamic" | "fetchCache" | "revalidate" => {
847                        if self.cache_components_enabled {
848                            possibly_invalid_exports.insert(
849                                export_name.clone(),
850                                (
851                                    InvalidExportKind::RouteSegmentConfig(
852                                        NextConfigProperty::CacheComponents,
853                                    ),
854                                    *span,
855                                ),
856                            );
857                        }
858                    }
859                    _ => (),
860                };
861
862            for export in &module.body {
863                match export {
864                    ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(export)) => {
865                        for specifier in &export.specifiers {
866                            if let ExportSpecifier::Named(named) = specifier {
867                                match &named.orig {
868                                    ModuleExportName::Ident(i) => {
869                                        collect_possibly_invalid_exports(&i.sym, &named.span);
870                                    }
871                                    ModuleExportName::Str(s) => {
872                                        collect_possibly_invalid_exports(&s.value, &named.span);
873                                    }
874                                }
875                            }
876                        }
877                    }
878                    ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export)) => match &export.decl {
879                        Decl::Fn(f) => {
880                            collect_possibly_invalid_exports(&f.ident.sym, &f.ident.span);
881                        }
882                        Decl::Var(v) => {
883                            for decl in &v.decls {
884                                if let Pat::Ident(i) = &decl.name {
885                                    collect_possibly_invalid_exports(&i.sym, &i.span);
886                                }
887                            }
888                        }
889                        _ => {}
890                    },
891                    _ => {}
892                }
893            }
894
895            for (export_name, (kind, span)) in &possibly_invalid_exports {
896                match kind {
897                    InvalidExportKind::RouteSegmentConfig(property) => {
898                        report_error(
899                            &self.app_dir,
900                            &self.filepath,
901                            RSCErrorKind::NextRscErrIncompatibleRouteSegmentConfig(
902                                *span,
903                                export_name.to_string(),
904                                *property,
905                            ),
906                        );
907                    }
908                    InvalidExportKind::Metadata => {
909                        // Client entry can't export `generateMetadata` or `metadata`.
910                        if is_client_entry
911                            && (export_name == "generateMetadata" || export_name == "metadata")
912                        {
913                            report_error(
914                                &self.app_dir,
915                                &self.filepath,
916                                RSCErrorKind::NextRscErrClientMetadataExport((
917                                    export_name.to_string(),
918                                    *span,
919                                )),
920                            );
921                        }
922                        // Server entry can't export `generateMetadata` and `metadata` together,
923                        // which is handled separately below.
924                    }
925                    InvalidExportKind::General => {
926                        report_error(
927                            &self.app_dir,
928                            &self.filepath,
929                            RSCErrorKind::NextRscErrInvalidApi((export_name.to_string(), *span)),
930                        );
931                    }
932                }
933            }
934
935            // Server entry can't export `generateMetadata` and `metadata` together.
936            if !is_client_entry {
937                let export1 = possibly_invalid_exports.get(&atom!("generateMetadata"));
938                let export2 = possibly_invalid_exports.get(&atom!("metadata"));
939
940                if let (Some((_, span1)), Some((_, span2))) = (export1, export2) {
941                    report_error(
942                        &self.app_dir,
943                        &self.filepath,
944                        RSCErrorKind::NextRscErrConflictMetadataExport((*span1, *span2)),
945                    );
946                }
947            }
948        }
949    }
950
951    /// ```js
952    /// import dynamic from 'next/dynamic'
953    ///
954    /// dynamic(() => import(...)) // ✅
955    /// dynamic(() => import(...), { ssr: true }) // ✅
956    /// dynamic(() => import(...), { ssr: false }) // ❌
957    /// ```
958    fn check_for_next_ssr_false(&self, node: &CallExpr) -> Option<()> {
959        if !self.is_callee_next_dynamic(&node.callee) {
960            return None;
961        }
962
963        let ssr_arg = node.args.get(1)?;
964        let obj = ssr_arg.expr.as_object()?;
965
966        for prop in obj.props.iter().filter_map(|v| v.as_prop()?.as_key_value()) {
967            let is_ssr = match &prop.key {
968                PropName::Ident(IdentName { sym, .. }) => sym == "ssr",
969                PropName::Str(s) => s.value == "ssr",
970                _ => false,
971            };
972
973            if is_ssr {
974                let value = prop.value.as_lit()?;
975                if let Lit::Bool(Bool { value: false, .. }) = value {
976                    report_error(
977                        &self.app_dir,
978                        &self.filepath,
979                        RSCErrorKind::NextSsrDynamicFalseNotAllowed(node.span),
980                    );
981                }
982            }
983        }
984
985        None
986    }
987}
988
989impl Visit for ReactServerComponentValidator {
990    noop_visit_type!();
991
992    // coerce parsed script to run validation for the context, which is still
993    // required even if file is empty
994    fn visit_script(&mut self, script: &swc_core::ecma::ast::Script) {
995        if script.body.is_empty() {
996            self.visit_module(&Module::dummy());
997        }
998    }
999
1000    fn visit_call_expr(&mut self, node: &CallExpr) {
1001        node.visit_children_with(self);
1002
1003        if self.is_react_server_layer {
1004            self.check_for_next_ssr_false(node);
1005        }
1006    }
1007
1008    fn visit_module(&mut self, module: &Module) {
1009        self.imports = ImportMap::analyze(module);
1010
1011        let (is_client_entry, is_action_file, imports, export_names) =
1012            collect_top_level_directives_and_imports(&self.app_dir, &self.filepath, module);
1013        let imports = Rc::new(imports);
1014        let export_names = Rc::new(export_names);
1015
1016        self.directive_import_collection = Some((
1017            is_client_entry,
1018            is_action_file,
1019            imports.clone(),
1020            export_names,
1021        ));
1022
1023        if self.is_react_server_layer {
1024            if is_client_entry {
1025                return;
1026            } else {
1027                // Only assert server graph if file's bundle target is "server", e.g.
1028                // * server components pages
1029                // * pages bundles on SSR layer
1030                // * middleware
1031                // * app/pages api routes
1032                self.assert_server_graph(&imports, module);
1033            }
1034        } else {
1035            // Only assert client graph if the file is not an action file,
1036            // and bundle target is "client" e.g.
1037            // * client components pages
1038            // * pages bundles on browser layer
1039            if !is_action_file {
1040                self.assert_client_graph(&imports);
1041                self.assert_invalid_api(module, true);
1042            }
1043        }
1044
1045        module.visit_children_with(self);
1046    }
1047}
1048
1049/// Returns a visitor to assert react server components without any transform.
1050/// This is for the Turbopack which have its own transform phase for the server
1051/// components proxy.
1052///
1053/// This also returns a visitor instead of fold and performs better than running
1054/// whole transform as a folder.
1055pub fn server_components_assert(
1056    filename: FileName,
1057    config: Config,
1058    app_dir: Option<PathBuf>,
1059) -> impl Visit {
1060    let is_react_server_layer: bool = match &config {
1061        Config::WithOptions(x) => x.is_react_server_layer,
1062        _ => false,
1063    };
1064    let cache_components_enabled: bool = match &config {
1065        Config::WithOptions(x) => x.cache_components_enabled,
1066        _ => false,
1067    };
1068    let use_cache_enabled: bool = match &config {
1069        Config::WithOptions(x) => x.use_cache_enabled,
1070        _ => false,
1071    };
1072    let filename = match filename {
1073        FileName::Custom(path) => format!("<{path}>"),
1074        _ => filename.to_string(),
1075    };
1076    ReactServerComponentValidator::new(
1077        is_react_server_layer,
1078        cache_components_enabled,
1079        use_cache_enabled,
1080        filename,
1081        app_dir,
1082    )
1083}
1084
1085/// Runs react server component transform for the module proxy, as well as
1086/// running assertion.
1087pub fn server_components<C: Comments>(
1088    filename: Arc<FileName>,
1089    config: Config,
1090    comments: C,
1091    app_dir: Option<PathBuf>,
1092) -> impl Pass + VisitMut {
1093    let is_react_server_layer: bool = match &config {
1094        Config::WithOptions(x) => x.is_react_server_layer,
1095        _ => false,
1096    };
1097    let cache_components_enabled: bool = match &config {
1098        Config::WithOptions(x) => x.cache_components_enabled,
1099        _ => false,
1100    };
1101    let use_cache_enabled: bool = match &config {
1102        Config::WithOptions(x) => x.use_cache_enabled,
1103        _ => false,
1104    };
1105    visit_mut_pass(ReactServerComponents {
1106        is_react_server_layer,
1107        cache_components_enabled,
1108        use_cache_enabled,
1109        comments,
1110        filepath: match &*filename {
1111            FileName::Custom(path) => format!("<{path}>"),
1112            _ => filename.to_string(),
1113        },
1114        app_dir,
1115        directive_import_collection: None,
1116    })
1117}