Skip to main content

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