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, Wtf8Atom},
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: (Wtf8Atom, 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) => (imported.atom().into_owned(), imported.span()),
489                            None => (named.local.to_id().0, named.local.span),
490                        },
491                        ImportSpecifier::Default(d) => (atom!(""), d.span),
492                        ImportSpecifier::Namespace(n) => (atom!("*"), n.span),
493                    })
494                    .collect();
495
496                imports.push(ModuleImports {
497                    source: (source, import.span),
498                    specifiers,
499                });
500
501                finished_directives = true;
502            }
503            // Collect all export names.
504            ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(e)) => {
505                for specifier in &e.specifiers {
506                    export_names.push(match specifier {
507                        ExportSpecifier::Default(_) => atom!("default"),
508                        ExportSpecifier::Namespace(_) => atom!("*"),
509                        ExportSpecifier::Named(named) => named
510                            .exported
511                            .as_ref()
512                            .unwrap_or(&named.orig)
513                            .atom()
514                            .into_owned(),
515                    })
516                }
517                finished_directives = true;
518            }
519            ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl { decl, .. })) => {
520                match decl {
521                    Decl::Class(ClassDecl { ident, .. }) => {
522                        export_names.push(ident.sym.clone());
523                    }
524                    Decl::Fn(FnDecl { ident, .. }) => {
525                        export_names.push(ident.sym.clone());
526                    }
527                    Decl::Var(var) => {
528                        for decl in &var.decls {
529                            if let Pat::Ident(ident) = &decl.name {
530                                export_names.push(ident.id.sym.clone());
531                            }
532                        }
533                    }
534                    _ => {}
535                }
536                finished_directives = true;
537            }
538            ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(ExportDefaultDecl {
539                decl: _,
540                ..
541            })) => {
542                export_names.push(atom!("default"));
543                finished_directives = true;
544            }
545            ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(ExportDefaultExpr {
546                expr: _,
547                ..
548            })) => {
549                export_names.push(atom!("default"));
550                finished_directives = true;
551            }
552            ModuleItem::ModuleDecl(ModuleDecl::ExportAll(_)) => {
553                export_names.push(atom!("*"));
554            }
555            _ => {
556                finished_directives = true;
557            }
558        }
559    });
560
561    let directive = if is_client_entry {
562        Some(ModuleDirective::UseClient)
563    } else if is_action_file {
564        Some(ModuleDirective::UseServer)
565    } else if is_cache_file {
566        Some(ModuleDirective::UseCache)
567    } else {
568        None
569    };
570
571    (directive, imports, export_names)
572}
573
574/// A visitor to assert given module file is a valid React server component.
575struct ReactServerComponentValidator {
576    is_react_server_layer: bool,
577    cache_components_enabled: bool,
578    use_cache_enabled: bool,
579    filepath: String,
580    app_dir: Option<PathBuf>,
581    invalid_server_imports: Vec<Wtf8Atom>,
582    invalid_server_lib_apis_mapping: FxHashMap<Wtf8Atom, Vec<&'static str>>,
583    deprecated_apis_mapping: FxHashMap<Wtf8Atom, Vec<&'static str>>,
584    invalid_client_imports: Vec<Wtf8Atom>,
585    invalid_client_lib_apis_mapping: FxHashMap<Wtf8Atom, Vec<&'static str>>,
586    pub module_directive: Option<ModuleDirective>,
587    pub export_names: Vec<Atom>,
588    imports: ImportMap,
589}
590
591impl ReactServerComponentValidator {
592    pub fn new(
593        is_react_server_layer: bool,
594        cache_components_enabled: bool,
595        use_cache_enabled: bool,
596        filename: String,
597        app_dir: Option<PathBuf>,
598    ) -> Self {
599        Self {
600            is_react_server_layer,
601            cache_components_enabled,
602            use_cache_enabled,
603            filepath: filename,
604            app_dir,
605            module_directive: None,
606            export_names: vec![],
607            // react -> [apis]
608            // react-dom -> [apis]
609            // next/navigation -> [apis]
610            invalid_server_lib_apis_mapping: FxHashMap::from_iter([
611                (
612                    atom!("react").into(),
613                    vec![
614                        "Component",
615                        "createContext",
616                        "createFactory",
617                        "PureComponent",
618                        "useDeferredValue",
619                        "useEffect",
620                        "useImperativeHandle",
621                        "useInsertionEffect",
622                        "useLayoutEffect",
623                        "useReducer",
624                        "useRef",
625                        "useState",
626                        "useSyncExternalStore",
627                        "useTransition",
628                        "useOptimistic",
629                        "useActionState",
630                        "experimental_useOptimistic",
631                    ],
632                ),
633                (
634                    atom!("react-dom").into(),
635                    vec![
636                        "flushSync",
637                        "unstable_batchedUpdates",
638                        "useFormStatus",
639                        "useFormState",
640                    ],
641                ),
642                (
643                    atom!("next/navigation").into(),
644                    vec![
645                        "useSearchParams",
646                        "usePathname",
647                        "useSelectedLayoutSegment",
648                        "useSelectedLayoutSegments",
649                        "useParams",
650                        "useRouter",
651                        "useServerInsertedHTML",
652                        "ServerInsertedHTMLContext",
653                        "unstable_isUnrecognizedActionError",
654                    ],
655                ),
656                (atom!("next/link").into(), vec!["useLinkStatus"]),
657            ]),
658            deprecated_apis_mapping: FxHashMap::from_iter([(
659                atom!("next/server").into(),
660                vec!["ImageResponse"],
661            )]),
662
663            invalid_server_imports: vec![
664                atom!("client-only").into(),
665                atom!("react-dom/client").into(),
666                atom!("react-dom/server").into(),
667                atom!("next/router").into(),
668            ],
669
670            invalid_client_imports: vec![
671                atom!("server-only").into(),
672                atom!("next/headers").into(),
673                atom!("next/root-params").into(),
674            ],
675
676            invalid_client_lib_apis_mapping: FxHashMap::from_iter([
677                (atom!("next/server").into(), vec!["after"]),
678                (
679                    atom!("next/cache").into(),
680                    vec![
681                        "revalidatePath",
682                        "revalidateTag",
683                        // "unstable_cache", // useless in client, but doesn't technically error
684                        "cacheLife",
685                        "unstable_cacheLife",
686                        "cacheTag",
687                        "unstable_cacheTag",
688                        // "unstable_noStore" // no-op in client, but allowed for legacy reasons
689                    ],
690                ),
691            ]),
692            imports: ImportMap::default(),
693        }
694    }
695
696    fn is_from_node_modules(&self, filepath: &str) -> bool {
697        static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"node_modules[\\/]").unwrap());
698        RE.is_match(filepath)
699    }
700
701    fn is_callee_next_dynamic(&self, callee: &Callee) -> bool {
702        match callee {
703            Callee::Expr(expr) => self.imports.is_import(expr, "next/dynamic", "default"),
704            _ => false,
705        }
706    }
707
708    // Asserts the server lib apis
709    // e.g.
710    // assert_invalid_server_lib_apis("react", import)
711    // assert_invalid_server_lib_apis("react-dom", import)
712    fn assert_invalid_server_lib_apis(&self, import_source: &Wtf8Atom, import: &ModuleImports) {
713        let deprecated_apis = self.deprecated_apis_mapping.get(import_source);
714        if let Some(deprecated_apis) = deprecated_apis {
715            for specifier in &import.specifiers {
716                if deprecated_apis.contains(&specifier.0.as_str()) {
717                    report_error(
718                        &self.app_dir,
719                        &self.filepath,
720                        RSCErrorKind::NextRscErrDeprecatedApi((
721                            import_source.to_string_lossy().into_owned(),
722                            specifier.0.to_string(),
723                            specifier.1,
724                        )),
725                    );
726                }
727            }
728        }
729
730        let invalid_apis = self.invalid_server_lib_apis_mapping.get(import_source);
731        if let Some(invalid_apis) = invalid_apis {
732            for specifier in &import.specifiers {
733                if invalid_apis.contains(&specifier.0.as_str()) {
734                    report_error(
735                        &self.app_dir,
736                        &self.filepath,
737                        RSCErrorKind::NextRscErrReactApi((specifier.0.to_string(), specifier.1)),
738                    );
739                }
740            }
741        }
742    }
743
744    fn assert_server_graph(&self, imports: &[ModuleImports], module: &Module) {
745        // If the
746        if self.is_from_node_modules(&self.filepath) {
747            return;
748        }
749        for import in imports {
750            let source = &import.source.0;
751            if self.invalid_server_imports.contains(source) {
752                report_error(
753                    &self.app_dir,
754                    &self.filepath,
755                    RSCErrorKind::NextRscErrServerImport((
756                        source.to_string_lossy().into_owned(),
757                        import.source.1,
758                    )),
759                );
760            }
761
762            self.assert_invalid_server_lib_apis(source, import);
763        }
764
765        self.assert_invalid_api(module, false);
766        self.assert_server_filename(module);
767    }
768
769    fn assert_server_filename(&self, module: &Module) {
770        if self.is_from_node_modules(&self.filepath) {
771            return;
772        }
773        static RE: Lazy<Regex> =
774            Lazy::new(|| Regex::new(r"[\\/]((global-)?error)\.(ts|js)x?$").unwrap());
775
776        let is_error_file = RE.is_match(&self.filepath);
777
778        if is_error_file {
779            if let Some(app_dir) = &self.app_dir {
780                if let Some(app_dir) = app_dir.to_str() {
781                    if self.filepath.starts_with(app_dir) {
782                        let span = if let Some(first_item) = module.body.first() {
783                            first_item.span()
784                        } else {
785                            module.span
786                        };
787
788                        report_error(
789                            &self.app_dir,
790                            &self.filepath,
791                            RSCErrorKind::NextRscErrErrorFileServerComponent(span),
792                        );
793                    }
794                }
795            }
796        }
797    }
798
799    fn assert_client_graph(&self, imports: &[ModuleImports]) {
800        if self.is_from_node_modules(&self.filepath) {
801            return;
802        }
803        for import in imports {
804            let source = &import.source.0;
805
806            if self.invalid_client_imports.contains(source) {
807                report_error(
808                    &self.app_dir,
809                    &self.filepath,
810                    RSCErrorKind::NextRscErrClientImport((
811                        source.to_string_lossy().into_owned(),
812                        import.source.1,
813                    )),
814                );
815            }
816
817            let invalid_apis = self.invalid_client_lib_apis_mapping.get(source);
818            if let Some(invalid_apis) = invalid_apis {
819                for specifier in &import.specifiers {
820                    if invalid_apis.contains(&specifier.0.as_str()) {
821                        report_error(
822                            &self.app_dir,
823                            &self.filepath,
824                            RSCErrorKind::NextRscErrClientImport((
825                                specifier.0.to_string(),
826                                specifier.1,
827                            )),
828                        );
829                    }
830                }
831            }
832        }
833    }
834
835    fn assert_invalid_api(&self, module: &Module, is_client_entry: bool) {
836        if self.is_from_node_modules(&self.filepath) {
837            return;
838        }
839        static RE: Lazy<Regex> =
840            Lazy::new(|| Regex::new(r"[\\/](page|layout|route)\.(ts|js)x?$").unwrap());
841        let is_app_entry = RE.is_match(&self.filepath);
842
843        if is_app_entry {
844            let mut possibly_invalid_exports: FxIndexMap<Atom, (InvalidExportKind, Span)> =
845                FxIndexMap::default();
846
847            let mut collect_possibly_invalid_exports =
848                |export_name: &Atom, span: &Span| match &**export_name {
849                    "getServerSideProps" | "getStaticProps" => {
850                        possibly_invalid_exports
851                            .insert(export_name.clone(), (InvalidExportKind::General, *span));
852                    }
853                    "generateMetadata" | "metadata" => {
854                        possibly_invalid_exports
855                            .insert(export_name.clone(), (InvalidExportKind::Metadata, *span));
856                    }
857                    "runtime" => {
858                        if self.cache_components_enabled {
859                            possibly_invalid_exports.insert(
860                                export_name.clone(),
861                                (
862                                    InvalidExportKind::RouteSegmentConfig(
863                                        NextConfigProperty::CacheComponents,
864                                    ),
865                                    *span,
866                                ),
867                            );
868                        } else if self.use_cache_enabled {
869                            possibly_invalid_exports.insert(
870                                export_name.clone(),
871                                (
872                                    InvalidExportKind::RouteSegmentConfig(
873                                        NextConfigProperty::UseCache,
874                                    ),
875                                    *span,
876                                ),
877                            );
878                        }
879                    }
880                    "dynamicParams" | "dynamic" | "fetchCache" | "revalidate"
881                    | "experimental_ppr" => {
882                        if self.cache_components_enabled {
883                            possibly_invalid_exports.insert(
884                                export_name.clone(),
885                                (
886                                    InvalidExportKind::RouteSegmentConfig(
887                                        NextConfigProperty::CacheComponents,
888                                    ),
889                                    *span,
890                                ),
891                            );
892                        }
893                    }
894                    _ => (),
895                };
896
897            for export in &module.body {
898                match export {
899                    ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(export)) => {
900                        for specifier in &export.specifiers {
901                            if let ExportSpecifier::Named(named) = specifier {
902                                collect_possibly_invalid_exports(&named.orig.atom(), &named.span);
903                            }
904                        }
905                    }
906                    ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export)) => match &export.decl {
907                        Decl::Fn(f) => {
908                            collect_possibly_invalid_exports(&f.ident.sym, &f.ident.span);
909                        }
910                        Decl::Var(v) => {
911                            for decl in &v.decls {
912                                if let Pat::Ident(i) = &decl.name {
913                                    collect_possibly_invalid_exports(&i.sym, &i.span);
914                                }
915                            }
916                        }
917                        _ => {}
918                    },
919                    _ => {}
920                }
921            }
922
923            for (export_name, (kind, span)) in &possibly_invalid_exports {
924                match kind {
925                    InvalidExportKind::RouteSegmentConfig(property) => {
926                        report_error(
927                            &self.app_dir,
928                            &self.filepath,
929                            RSCErrorKind::NextRscErrIncompatibleRouteSegmentConfig(
930                                *span,
931                                export_name.to_string(),
932                                *property,
933                            ),
934                        );
935                    }
936                    InvalidExportKind::Metadata => {
937                        // Client entry can't export `generateMetadata` or `metadata`.
938                        if is_client_entry
939                            && (export_name == "generateMetadata" || export_name == "metadata")
940                        {
941                            report_error(
942                                &self.app_dir,
943                                &self.filepath,
944                                RSCErrorKind::NextRscErrClientMetadataExport((
945                                    export_name.to_string(),
946                                    *span,
947                                )),
948                            );
949                        }
950                        // Server entry can't export `generateMetadata` and `metadata` together,
951                        // which is handled separately below.
952                    }
953                    InvalidExportKind::General => {
954                        report_error(
955                            &self.app_dir,
956                            &self.filepath,
957                            RSCErrorKind::NextRscErrInvalidApi((export_name.to_string(), *span)),
958                        );
959                    }
960                }
961            }
962
963            // Server entry can't export `generateMetadata` and `metadata` together.
964            if !is_client_entry {
965                let export1 = possibly_invalid_exports.get(&atom!("generateMetadata"));
966                let export2 = possibly_invalid_exports.get(&atom!("metadata"));
967
968                if let (Some((_, span1)), Some((_, span2))) = (export1, export2) {
969                    report_error(
970                        &self.app_dir,
971                        &self.filepath,
972                        RSCErrorKind::NextRscErrConflictMetadataExport((*span1, *span2)),
973                    );
974                }
975            }
976        }
977    }
978
979    /// ```js
980    /// import dynamic from 'next/dynamic'
981    ///
982    /// dynamic(() => import(...)) // ✅
983    /// dynamic(() => import(...), { ssr: true }) // ✅
984    /// dynamic(() => import(...), { ssr: false }) // ❌
985    /// ```
986    fn check_for_next_ssr_false(&self, node: &CallExpr) -> Option<()> {
987        if !self.is_callee_next_dynamic(&node.callee) {
988            return None;
989        }
990
991        let ssr_arg = node.args.get(1)?;
992        let obj = ssr_arg.expr.as_object()?;
993
994        for prop in obj.props.iter().filter_map(|v| v.as_prop()?.as_key_value()) {
995            let is_ssr = match &prop.key {
996                PropName::Ident(IdentName { sym, .. }) => sym == "ssr",
997                PropName::Str(s) => s.value == "ssr",
998                _ => false,
999            };
1000
1001            if is_ssr {
1002                let value = prop.value.as_lit()?;
1003                if let Lit::Bool(Bool { value: false, .. }) = value {
1004                    report_error(
1005                        &self.app_dir,
1006                        &self.filepath,
1007                        RSCErrorKind::NextSsrDynamicFalseNotAllowed(node.span),
1008                    );
1009                }
1010            }
1011        }
1012
1013        None
1014    }
1015}
1016
1017impl Visit for ReactServerComponentValidator {
1018    noop_visit_type!();
1019
1020    // coerce parsed script to run validation for the context, which is still
1021    // required even if file is empty
1022    fn visit_script(&mut self, script: &swc_core::ecma::ast::Script) {
1023        if script.body.is_empty() {
1024            self.visit_module(&Module::dummy());
1025        }
1026    }
1027
1028    fn visit_call_expr(&mut self, node: &CallExpr) {
1029        node.visit_children_with(self);
1030
1031        if self.is_react_server_layer {
1032            self.check_for_next_ssr_false(node);
1033        }
1034    }
1035
1036    fn visit_module(&mut self, module: &Module) {
1037        self.imports = ImportMap::analyze(module);
1038
1039        let (directive, imports, export_names) =
1040            collect_module_info(&self.app_dir, &self.filepath, module);
1041        let imports = Rc::new(imports);
1042
1043        self.module_directive = directive;
1044        self.export_names = export_names;
1045
1046        if self.is_react_server_layer {
1047            if directive == Some(ModuleDirective::UseClient) {
1048                return;
1049            } else {
1050                // Only assert server graph if file's bundle target is "server", e.g.
1051                // * server components pages
1052                // * pages bundles on SSR layer
1053                // * middleware
1054                // * app/pages api routes
1055                self.assert_server_graph(&imports, module);
1056            }
1057        } else {
1058            // Only assert client graph if the file is not an action or cache file,
1059            // and bundle target is "client" e.g.
1060            // * client components pages
1061            // * pages bundles on browser layer
1062            if directive != Some(ModuleDirective::UseServer)
1063                && directive != Some(ModuleDirective::UseCache)
1064            {
1065                self.assert_client_graph(&imports);
1066                self.assert_invalid_api(module, true);
1067            }
1068        }
1069
1070        module.visit_children_with(self);
1071    }
1072}
1073
1074/// Returns a visitor to assert react server components without any transform.
1075/// This is for the Turbopack which have its own transform phase for the server
1076/// components proxy.
1077///
1078/// This also returns a visitor instead of fold and performs better than running
1079/// whole transform as a folder.
1080pub fn server_components_assert(
1081    filename: FileName,
1082    config: Config,
1083    app_dir: Option<PathBuf>,
1084) -> impl Visit {
1085    let is_react_server_layer: bool = match &config {
1086        Config::WithOptions(x) => x.is_react_server_layer,
1087        _ => false,
1088    };
1089    let cache_components_enabled: bool = match &config {
1090        Config::WithOptions(x) => x.cache_components_enabled,
1091        _ => false,
1092    };
1093    let use_cache_enabled: bool = match &config {
1094        Config::WithOptions(x) => x.use_cache_enabled,
1095        _ => false,
1096    };
1097    let filename = match filename {
1098        FileName::Custom(path) => format!("<{path}>"),
1099        _ => filename.to_string(),
1100    };
1101    ReactServerComponentValidator::new(
1102        is_react_server_layer,
1103        cache_components_enabled,
1104        use_cache_enabled,
1105        filename,
1106        app_dir,
1107    )
1108}
1109
1110/// Runs react server component transform for the module proxy, as well as
1111/// running assertion.
1112pub fn server_components<C: Comments>(
1113    filename: Arc<FileName>,
1114    config: Config,
1115    comments: C,
1116    app_dir: Option<PathBuf>,
1117) -> impl Pass + VisitMut {
1118    let is_react_server_layer: bool = match &config {
1119        Config::WithOptions(x) => x.is_react_server_layer,
1120        _ => false,
1121    };
1122    let cache_components_enabled: bool = match &config {
1123        Config::WithOptions(x) => x.cache_components_enabled,
1124        _ => false,
1125    };
1126    let use_cache_enabled: bool = match &config {
1127        Config::WithOptions(x) => x.use_cache_enabled,
1128        _ => false,
1129    };
1130    visit_mut_pass(ReactServerComponents {
1131        is_react_server_layer,
1132        cache_components_enabled,
1133        use_cache_enabled,
1134        comments,
1135        filepath: match &*filename {
1136            FileName::Custom(path) => format!("<{path}>"),
1137            _ => filename.to_string(),
1138        },
1139        app_dir,
1140    })
1141}