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