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