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 dynamic_io_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    dynamic_io_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    DynamicIo,
98    UseCache,
99}
100
101impl Display for NextConfigProperty {
102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103        match self {
104            NextConfigProperty::DynamicIo => write!(f, "experimental.dynamicIO"),
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.dynamic_io_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        let _ = &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    dynamic_io_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        dynamic_io_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            dynamic_io_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                    ],
628                ),
629                ("next/link", vec!["useLinkStatus"]),
630            ]),
631            deprecated_apis_mapping: FxHashMap::from_iter([("next/server", vec!["ImageResponse"])]),
632
633            invalid_server_imports: vec![
634                Atom::from("client-only"),
635                Atom::from("react-dom/client"),
636                Atom::from("react-dom/server"),
637                Atom::from("next/router"),
638            ],
639
640            invalid_client_imports: vec![Atom::from("server-only"), Atom::from("next/headers")],
641
642            invalid_client_lib_apis_mapping: FxHashMap::from_iter([
643                ("next/server", vec!["after", "unstable_rootParams"]),
644                (
645                    "next/cache",
646                    vec![
647                        "revalidatePath",
648                        "revalidateTag",
649                        // "unstable_cache", // useless in client, but doesn't technically error
650                        "unstable_cacheLife",
651                        "unstable_cacheTag",
652                        "unstable_expirePath",
653                        "unstable_expireTag",
654                        // "unstable_noStore" // no-op in client, but allowed for legacy reasons
655                    ],
656                ),
657            ]),
658            imports: ImportMap::default(),
659        }
660    }
661
662    fn is_from_node_modules(&self, filepath: &str) -> bool {
663        static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"node_modules[\\/]").unwrap());
664        RE.is_match(filepath)
665    }
666
667    fn is_callee_next_dynamic(&self, callee: &Callee) -> bool {
668        match callee {
669            Callee::Expr(expr) => self.imports.is_import(expr, "next/dynamic", "default"),
670            _ => false,
671        }
672    }
673
674    // Asserts the server lib apis
675    // e.g.
676    // assert_invalid_server_lib_apis("react", import)
677    // assert_invalid_server_lib_apis("react-dom", import)
678    fn assert_invalid_server_lib_apis(&self, import_source: String, import: &ModuleImports) {
679        let deprecated_apis = self.deprecated_apis_mapping.get(import_source.as_str());
680        if let Some(deprecated_apis) = deprecated_apis {
681            for specifier in &import.specifiers {
682                if deprecated_apis.contains(&specifier.0.as_str()) {
683                    report_error(
684                        &self.app_dir,
685                        &self.filepath,
686                        RSCErrorKind::NextRscErrDeprecatedApi((
687                            import_source.clone(),
688                            specifier.0.to_string(),
689                            specifier.1,
690                        )),
691                    );
692                }
693            }
694        }
695
696        let invalid_apis = self
697            .invalid_server_lib_apis_mapping
698            .get(import_source.as_str());
699        if let Some(invalid_apis) = invalid_apis {
700            for specifier in &import.specifiers {
701                if invalid_apis.contains(&specifier.0.as_str()) {
702                    report_error(
703                        &self.app_dir,
704                        &self.filepath,
705                        RSCErrorKind::NextRscErrReactApi((specifier.0.to_string(), specifier.1)),
706                    );
707                }
708            }
709        }
710    }
711
712    fn assert_server_graph(&self, imports: &[ModuleImports], module: &Module) {
713        // If the
714        if self.is_from_node_modules(&self.filepath) {
715            return;
716        }
717        for import in imports {
718            let source = import.source.0.clone();
719            let source_str = source.to_string();
720            if self.invalid_server_imports.contains(&source) {
721                report_error(
722                    &self.app_dir,
723                    &self.filepath,
724                    RSCErrorKind::NextRscErrServerImport((source_str.clone(), import.source.1)),
725                );
726            }
727
728            self.assert_invalid_server_lib_apis(source_str, import);
729        }
730
731        self.assert_invalid_api(module, false);
732        self.assert_server_filename(module);
733    }
734
735    fn assert_server_filename(&self, module: &Module) {
736        if self.is_from_node_modules(&self.filepath) {
737            return;
738        }
739        static RE: Lazy<Regex> =
740            Lazy::new(|| Regex::new(r"[\\/]((global-)?error)\.(ts|js)x?$").unwrap());
741
742        let is_error_file = RE.is_match(&self.filepath);
743
744        if is_error_file {
745            if let Some(app_dir) = &self.app_dir {
746                if let Some(app_dir) = app_dir.to_str() {
747                    if self.filepath.starts_with(app_dir) {
748                        let span = if let Some(first_item) = module.body.first() {
749                            first_item.span()
750                        } else {
751                            module.span
752                        };
753
754                        report_error(
755                            &self.app_dir,
756                            &self.filepath,
757                            RSCErrorKind::NextRscErrErrorFileServerComponent(span),
758                        );
759                    }
760                }
761            }
762        }
763    }
764
765    fn assert_client_graph(&self, imports: &[ModuleImports]) {
766        if self.is_from_node_modules(&self.filepath) {
767            return;
768        }
769        for import in imports {
770            let source = &import.source.0;
771
772            if self.invalid_client_imports.contains(source) {
773                report_error(
774                    &self.app_dir,
775                    &self.filepath,
776                    RSCErrorKind::NextRscErrClientImport((source.to_string(), import.source.1)),
777                );
778            }
779
780            let invalid_apis = self.invalid_client_lib_apis_mapping.get(source.as_str());
781            if let Some(invalid_apis) = invalid_apis {
782                for specifier in &import.specifiers {
783                    if invalid_apis.contains(&specifier.0.as_str()) {
784                        report_error(
785                            &self.app_dir,
786                            &self.filepath,
787                            RSCErrorKind::NextRscErrClientImport((
788                                specifier.0.to_string(),
789                                specifier.1,
790                            )),
791                        );
792                    }
793                }
794            }
795        }
796    }
797
798    fn assert_invalid_api(&self, module: &Module, is_client_entry: bool) {
799        if self.is_from_node_modules(&self.filepath) {
800            return;
801        }
802        static RE: Lazy<Regex> =
803            Lazy::new(|| Regex::new(r"[\\/](page|layout)\.(ts|js)x?$").unwrap());
804        let is_layout_or_page = RE.is_match(&self.filepath);
805
806        if is_layout_or_page {
807            let mut possibly_invalid_exports: FxIndexMap<Atom, (InvalidExportKind, Span)> =
808                FxIndexMap::default();
809
810            let mut collect_possibly_invalid_exports =
811                |export_name: &Atom, span: &Span| match &**export_name {
812                    "getServerSideProps" | "getStaticProps" => {
813                        possibly_invalid_exports
814                            .insert(export_name.clone(), (InvalidExportKind::General, *span));
815                    }
816                    "generateMetadata" | "metadata" => {
817                        possibly_invalid_exports
818                            .insert(export_name.clone(), (InvalidExportKind::Metadata, *span));
819                    }
820                    "runtime" => {
821                        if self.dynamic_io_enabled {
822                            possibly_invalid_exports.insert(
823                                export_name.clone(),
824                                (
825                                    InvalidExportKind::RouteSegmentConfig(
826                                        NextConfigProperty::DynamicIo,
827                                    ),
828                                    *span,
829                                ),
830                            );
831                        } else if self.use_cache_enabled {
832                            possibly_invalid_exports.insert(
833                                export_name.clone(),
834                                (
835                                    InvalidExportKind::RouteSegmentConfig(
836                                        NextConfigProperty::UseCache,
837                                    ),
838                                    *span,
839                                ),
840                            );
841                        }
842                    }
843                    "dynamicParams" | "dynamic" | "fetchCache" | "revalidate" => {
844                        if self.dynamic_io_enabled {
845                            possibly_invalid_exports.insert(
846                                export_name.clone(),
847                                (
848                                    InvalidExportKind::RouteSegmentConfig(
849                                        NextConfigProperty::DynamicIo,
850                                    ),
851                                    *span,
852                                ),
853                            );
854                        }
855                    }
856                    _ => (),
857                };
858
859            for export in &module.body {
860                match export {
861                    ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(export)) => {
862                        for specifier in &export.specifiers {
863                            if let ExportSpecifier::Named(named) = specifier {
864                                match &named.orig {
865                                    ModuleExportName::Ident(i) => {
866                                        collect_possibly_invalid_exports(&i.sym, &named.span);
867                                    }
868                                    ModuleExportName::Str(s) => {
869                                        collect_possibly_invalid_exports(&s.value, &named.span);
870                                    }
871                                }
872                            }
873                        }
874                    }
875                    ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export)) => match &export.decl {
876                        Decl::Fn(f) => {
877                            collect_possibly_invalid_exports(&f.ident.sym, &f.ident.span);
878                        }
879                        Decl::Var(v) => {
880                            for decl in &v.decls {
881                                if let Pat::Ident(i) = &decl.name {
882                                    collect_possibly_invalid_exports(&i.sym, &i.span);
883                                }
884                            }
885                        }
886                        _ => {}
887                    },
888                    _ => {}
889                }
890            }
891
892            for (export_name, (kind, span)) in &possibly_invalid_exports {
893                match kind {
894                    InvalidExportKind::RouteSegmentConfig(property) => {
895                        report_error(
896                            &self.app_dir,
897                            &self.filepath,
898                            RSCErrorKind::NextRscErrIncompatibleRouteSegmentConfig(
899                                *span,
900                                export_name.to_string(),
901                                *property,
902                            ),
903                        );
904                    }
905                    InvalidExportKind::Metadata => {
906                        // Client entry can't export `generateMetadata` or `metadata`.
907                        if is_client_entry
908                            && (export_name == "generateMetadata" || export_name == "metadata")
909                        {
910                            report_error(
911                                &self.app_dir,
912                                &self.filepath,
913                                RSCErrorKind::NextRscErrClientMetadataExport((
914                                    export_name.to_string(),
915                                    *span,
916                                )),
917                            );
918                        }
919                        // Server entry can't export `generateMetadata` and `metadata` together,
920                        // which is handled separately below.
921                    }
922                    InvalidExportKind::General => {
923                        report_error(
924                            &self.app_dir,
925                            &self.filepath,
926                            RSCErrorKind::NextRscErrInvalidApi((export_name.to_string(), *span)),
927                        );
928                    }
929                }
930            }
931
932            // Server entry can't export `generateMetadata` and `metadata` together.
933            if !is_client_entry {
934                let export1 = possibly_invalid_exports.get(&atom!("generateMetadata"));
935                let export2 = possibly_invalid_exports.get(&atom!("metadata"));
936
937                if let (Some((_, span1)), Some((_, span2))) = (export1, export2) {
938                    report_error(
939                        &self.app_dir,
940                        &self.filepath,
941                        RSCErrorKind::NextRscErrConflictMetadataExport((*span1, *span2)),
942                    );
943                }
944            }
945        }
946    }
947
948    /// ```js
949    /// import dynamic from 'next/dynamic'
950    ///
951    /// dynamic(() => import(...)) // ✅
952    /// dynamic(() => import(...), { ssr: true }) // ✅
953    /// dynamic(() => import(...), { ssr: false }) // ❌
954    /// ```
955    fn check_for_next_ssr_false(&self, node: &CallExpr) -> Option<()> {
956        if !self.is_callee_next_dynamic(&node.callee) {
957            return None;
958        }
959
960        let ssr_arg = node.args.get(1)?;
961        let obj = ssr_arg.expr.as_object()?;
962
963        for prop in obj.props.iter().filter_map(|v| v.as_prop()?.as_key_value()) {
964            let is_ssr = match &prop.key {
965                PropName::Ident(IdentName { sym, .. }) => sym == "ssr",
966                PropName::Str(s) => s.value == "ssr",
967                _ => false,
968            };
969
970            if is_ssr {
971                let value = prop.value.as_lit()?;
972                if let Lit::Bool(Bool { value: false, .. }) = value {
973                    report_error(
974                        &self.app_dir,
975                        &self.filepath,
976                        RSCErrorKind::NextSsrDynamicFalseNotAllowed(node.span),
977                    );
978                }
979            }
980        }
981
982        None
983    }
984}
985
986impl Visit for ReactServerComponentValidator {
987    noop_visit_type!();
988
989    // coerce parsed script to run validation for the context, which is still
990    // required even if file is empty
991    fn visit_script(&mut self, script: &swc_core::ecma::ast::Script) {
992        if script.body.is_empty() {
993            self.visit_module(&Module::dummy());
994        }
995    }
996
997    fn visit_call_expr(&mut self, node: &CallExpr) {
998        node.visit_children_with(self);
999
1000        if self.is_react_server_layer {
1001            self.check_for_next_ssr_false(node);
1002        }
1003    }
1004
1005    fn visit_module(&mut self, module: &Module) {
1006        self.imports = ImportMap::analyze(module);
1007
1008        let (is_client_entry, is_action_file, imports, export_names) =
1009            collect_top_level_directives_and_imports(&self.app_dir, &self.filepath, module);
1010        let imports = Rc::new(imports);
1011        let export_names = Rc::new(export_names);
1012
1013        self.directive_import_collection = Some((
1014            is_client_entry,
1015            is_action_file,
1016            imports.clone(),
1017            export_names,
1018        ));
1019
1020        if self.is_react_server_layer {
1021            if is_client_entry {
1022                return;
1023            } else {
1024                // Only assert server graph if file's bundle target is "server", e.g.
1025                // * server components pages
1026                // * pages bundles on SSR layer
1027                // * middleware
1028                // * app/pages api routes
1029                self.assert_server_graph(&imports, module);
1030            }
1031        } else {
1032            // Only assert client graph if the file is not an action file,
1033            // and bundle target is "client" e.g.
1034            // * client components pages
1035            // * pages bundles on browser layer
1036            if !is_action_file {
1037                self.assert_client_graph(&imports);
1038                self.assert_invalid_api(module, true);
1039            }
1040        }
1041
1042        module.visit_children_with(self);
1043    }
1044}
1045
1046/// Returns a visitor to assert react server components without any transform.
1047/// This is for the Turbopack which have its own transform phase for the server
1048/// components proxy.
1049///
1050/// This also returns a visitor instead of fold and performs better than running
1051/// whole transform as a folder.
1052pub fn server_components_assert(
1053    filename: FileName,
1054    config: Config,
1055    app_dir: Option<PathBuf>,
1056) -> impl Visit {
1057    let is_react_server_layer: bool = match &config {
1058        Config::WithOptions(x) => x.is_react_server_layer,
1059        _ => false,
1060    };
1061    let dynamic_io_enabled: bool = match &config {
1062        Config::WithOptions(x) => x.dynamic_io_enabled,
1063        _ => false,
1064    };
1065    let use_cache_enabled: bool = match &config {
1066        Config::WithOptions(x) => x.use_cache_enabled,
1067        _ => false,
1068    };
1069    let filename = match filename {
1070        FileName::Custom(path) => format!("<{path}>"),
1071        _ => filename.to_string(),
1072    };
1073    ReactServerComponentValidator::new(
1074        is_react_server_layer,
1075        dynamic_io_enabled,
1076        use_cache_enabled,
1077        filename,
1078        app_dir,
1079    )
1080}
1081
1082/// Runs react server component transform for the module proxy, as well as
1083/// running assertion.
1084pub fn server_components<C: Comments>(
1085    filename: Arc<FileName>,
1086    config: Config,
1087    comments: C,
1088    app_dir: Option<PathBuf>,
1089) -> impl Pass + VisitMut {
1090    let is_react_server_layer: bool = match &config {
1091        Config::WithOptions(x) => x.is_react_server_layer,
1092        _ => false,
1093    };
1094    let dynamic_io_enabled: bool = match &config {
1095        Config::WithOptions(x) => x.dynamic_io_enabled,
1096        _ => false,
1097    };
1098    let use_cache_enabled: bool = match &config {
1099        Config::WithOptions(x) => x.use_cache_enabled,
1100        _ => false,
1101    };
1102    visit_mut_pass(ReactServerComponents {
1103        is_react_server_layer,
1104        dynamic_io_enabled,
1105        use_cache_enabled,
1106        comments,
1107        filepath: match &*filename {
1108            FileName::Custom(path) => format!("<{path}>"),
1109            _ => filename.to_string(),
1110        },
1111        app_dir,
1112        directive_import_collection: None,
1113    })
1114}