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                decl: _,
570                ..
571            })) => {
572                export_names.push(atom!("default"));
573                finished_directives = true;
574            }
575            ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(ExportDefaultExpr {
576                expr: _,
577                ..
578            })) => {
579                export_names.push(atom!("default"));
580                finished_directives = true;
581            }
582            ModuleItem::ModuleDecl(ModuleDecl::ExportAll(_)) => {
583                export_names.push(atom!("*"));
584            }
585            _ => {
586                finished_directives = true;
587            }
588        }
589    });
590
591    let directive = if is_client_entry {
592        Some(ModuleDirective::UseClient)
593    } else if is_action_file {
594        Some(ModuleDirective::UseServer)
595    } else if is_cache_file {
596        Some(ModuleDirective::UseCache)
597    } else {
598        None
599    };
600
601    (directive, imports, export_names)
602}
603
604/// A visitor to assert given module file is a valid React server component.
605struct ReactServerComponentValidator {
606    is_react_server_layer: bool,
607    cache_components_enabled: bool,
608    use_cache_enabled: bool,
609    taint_enabled: bool,
610    filepath: String,
611    app_dir: Option<PathBuf>,
612    invalid_server_imports: Vec<Wtf8Atom>,
613    invalid_server_lib_apis_mapping: FxHashMap<Wtf8Atom, Vec<&'static str>>,
614    deprecated_apis_mapping: FxHashMap<Wtf8Atom, Vec<&'static str>>,
615    invalid_client_imports: Vec<Wtf8Atom>,
616    invalid_client_lib_apis_mapping: FxHashMap<Wtf8Atom, Vec<&'static str>>,
617    /// React taint APIs that require `experimental.taint` config
618    react_taint_apis: Vec<&'static str>,
619    pub module_directive: Option<ModuleDirective>,
620    pub export_names: Vec<Atom>,
621    imports: ImportMap,
622    page_extensions: Vec<String>,
623}
624
625impl ReactServerComponentValidator {
626    pub fn new(
627        is_react_server_layer: bool,
628        cache_components_enabled: bool,
629        use_cache_enabled: bool,
630        taint_enabled: bool,
631        filename: String,
632        app_dir: Option<PathBuf>,
633        page_extensions: Vec<String>,
634    ) -> Self {
635        Self {
636            is_react_server_layer,
637            cache_components_enabled,
638            use_cache_enabled,
639            taint_enabled,
640            filepath: filename,
641            app_dir,
642            module_directive: None,
643            export_names: vec![],
644            // react -> [apis]
645            // react-dom -> [apis]
646            // next/navigation -> [apis]
647            invalid_server_lib_apis_mapping: FxHashMap::from_iter([
648                (
649                    atom!("react").into(),
650                    vec![
651                        "Component",
652                        "createContext",
653                        "createFactory",
654                        "PureComponent",
655                        "useDeferredValue",
656                        "useEffect",
657                        "useEffectEvent",
658                        "useImperativeHandle",
659                        "useInsertionEffect",
660                        "useLayoutEffect",
661                        "useReducer",
662                        "useRef",
663                        "useState",
664                        "useSyncExternalStore",
665                        "useTransition",
666                        "useOptimistic",
667                        "useActionState",
668                        "experimental_useOptimistic",
669                    ],
670                ),
671                (
672                    atom!("react-dom").into(),
673                    vec![
674                        "flushSync",
675                        "unstable_batchedUpdates",
676                        "useFormStatus",
677                        "useFormState",
678                    ],
679                ),
680                (atom!("next/error").into(), vec!["unstable_catchError"]),
681                (
682                    atom!("next/navigation").into(),
683                    vec![
684                        "useSearchParams",
685                        "usePathname",
686                        "useSelectedLayoutSegment",
687                        "useSelectedLayoutSegments",
688                        "useParams",
689                        "useRouter",
690                        "useServerInsertedHTML",
691                        "ServerInsertedHTMLContext",
692                        "unstable_isUnrecognizedActionError",
693                    ],
694                ),
695                (atom!("next/link").into(), vec!["useLinkStatus"]),
696            ]),
697            deprecated_apis_mapping: FxHashMap::from_iter([(
698                atom!("next/server").into(),
699                vec!["ImageResponse"],
700            )]),
701
702            invalid_server_imports: vec![
703                atom!("client-only").into(),
704                atom!("react-dom/client").into(),
705                atom!("react-dom/server").into(),
706                atom!("next/router").into(),
707            ],
708
709            invalid_client_imports: vec![
710                atom!("server-only").into(),
711                atom!("next/headers").into(),
712                atom!("next/root-params").into(),
713            ],
714
715            invalid_client_lib_apis_mapping: FxHashMap::from_iter([
716                (atom!("next/server").into(), vec!["after"]),
717                (
718                    atom!("next/cache").into(),
719                    vec![
720                        "revalidatePath",
721                        "revalidateTag",
722                        // "unstable_cache", // useless in client, but doesn't technically error
723                        "cacheLife",
724                        "unstable_cacheLife",
725                        "cacheTag",
726                        "unstable_cacheTag",
727                        // "unstable_noStore" // no-op in client, but allowed for legacy reasons
728                    ],
729                ),
730            ]),
731            react_taint_apis: vec![
732                "experimental_taintObjectReference",
733                "experimental_taintUniqueValue",
734            ],
735            imports: ImportMap::default(),
736            page_extensions,
737        }
738    }
739
740    fn is_from_node_modules(&self, filepath: &str) -> bool {
741        static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"node_modules[\\/]").unwrap());
742        RE.is_match(filepath)
743    }
744
745    fn is_callee_next_dynamic(&self, callee: &Callee) -> bool {
746        match callee {
747            Callee::Expr(expr) => self.imports.is_import(expr, "next/dynamic", "default"),
748            _ => false,
749        }
750    }
751
752    // Asserts the server lib apis
753    // e.g.
754    // assert_invalid_server_lib_apis("react", import)
755    // assert_invalid_server_lib_apis("react-dom", import)
756    fn assert_invalid_server_lib_apis(&self, import_source: &Wtf8Atom, import: &ModuleImports) {
757        let deprecated_apis = self.deprecated_apis_mapping.get(import_source);
758        if let Some(deprecated_apis) = deprecated_apis {
759            for specifier in &import.specifiers {
760                if deprecated_apis.contains(&specifier.0.as_str()) {
761                    report_error(
762                        &self.app_dir,
763                        &self.filepath,
764                        RSCErrorKind::NextRscErrDeprecatedApi((
765                            import_source.to_string_lossy().into_owned(),
766                            specifier.0.to_string(),
767                            specifier.1,
768                        )),
769                    );
770                }
771            }
772        }
773
774        let invalid_apis = self.invalid_server_lib_apis_mapping.get(import_source);
775        if let Some(invalid_apis) = invalid_apis {
776            for specifier in &import.specifiers {
777                if invalid_apis.contains(&specifier.0.as_str()) {
778                    report_error(
779                        &self.app_dir,
780                        &self.filepath,
781                        RSCErrorKind::NextRscErrReactApi((specifier.0.to_string(), specifier.1)),
782                    );
783                }
784            }
785        }
786    }
787
788    /// Check for React taint API imports when taint is not enabled
789    fn assert_react_taint_apis(&self, imports: &[ModuleImports]) {
790        // Skip check if taint is enabled or if file is from node_modules
791        if self.taint_enabled || self.is_from_node_modules(&self.filepath) {
792            return;
793        }
794
795        for import in imports {
796            let source = &import.source.0;
797            // Only check imports from 'react'
798            if source.as_str() != Some("react") {
799                continue;
800            }
801
802            for specifier in &import.specifiers {
803                if self.react_taint_apis.contains(&specifier.0.as_str()) {
804                    report_error(
805                        &self.app_dir,
806                        &self.filepath,
807                        RSCErrorKind::NextRscErrTaintWithoutConfig((
808                            specifier.0.to_string(),
809                            specifier.1,
810                        )),
811                    );
812                }
813            }
814        }
815    }
816
817    fn assert_server_graph(&self, imports: &[ModuleImports], module: &Module) {
818        // If the
819        if self.is_from_node_modules(&self.filepath) {
820            return;
821        }
822        for import in imports {
823            let source = &import.source.0;
824            if self.invalid_server_imports.contains(source) {
825                report_error(
826                    &self.app_dir,
827                    &self.filepath,
828                    RSCErrorKind::NextRscErrServerImport((
829                        source.to_string_lossy().into_owned(),
830                        import.source.1,
831                    )),
832                );
833            }
834
835            self.assert_invalid_server_lib_apis(source, import);
836        }
837
838        self.assert_invalid_api(module, false);
839        self.assert_server_filename(module);
840    }
841
842    fn assert_server_filename(&self, module: &Module) {
843        if self.is_from_node_modules(&self.filepath) {
844            return;
845        }
846        let ext_pattern = build_page_extensions_regex(&self.page_extensions);
847        let re = Regex::new(&format!(r"[\\/]((global-)?error)\.{ext_pattern}$")).unwrap();
848
849        let is_error_file = re.is_match(&self.filepath);
850
851        if is_error_file
852            && let Some(app_dir) = &self.app_dir
853            && let Some(app_dir) = app_dir.to_str()
854            && self.filepath.starts_with(app_dir)
855        {
856            let span = if let Some(first_item) = module.body.first() {
857                first_item.span()
858            } else {
859                module.span
860            };
861
862            report_error(
863                &self.app_dir,
864                &self.filepath,
865                RSCErrorKind::NextRscErrErrorFileServerComponent(span),
866            );
867        }
868    }
869
870    fn assert_client_graph(&self, imports: &[ModuleImports]) {
871        if self.is_from_node_modules(&self.filepath) {
872            return;
873        }
874        for import in imports {
875            let source = &import.source.0;
876
877            if self.invalid_client_imports.contains(source) {
878                report_error(
879                    &self.app_dir,
880                    &self.filepath,
881                    RSCErrorKind::NextRscErrClientImport((
882                        source.to_string_lossy().into_owned(),
883                        import.source.1,
884                    )),
885                );
886            }
887
888            let invalid_apis = self.invalid_client_lib_apis_mapping.get(source);
889            if let Some(invalid_apis) = invalid_apis {
890                for specifier in &import.specifiers {
891                    if invalid_apis.contains(&specifier.0.as_str()) {
892                        report_error(
893                            &self.app_dir,
894                            &self.filepath,
895                            RSCErrorKind::NextRscErrClientImport((
896                                specifier.0.to_string(),
897                                specifier.1,
898                            )),
899                        );
900                    }
901                }
902            }
903        }
904    }
905
906    fn assert_invalid_api(&self, module: &Module, is_client_entry: bool) {
907        if self.is_from_node_modules(&self.filepath) {
908            return;
909        }
910        let ext_pattern = build_page_extensions_regex(&self.page_extensions);
911        let re = Regex::new(&format!(r"[\\/](page|layout|route)\.{ext_pattern}$")).unwrap();
912        let is_app_entry = re.is_match(&self.filepath);
913
914        if is_app_entry {
915            let mut possibly_invalid_exports: FxIndexMap<Atom, (InvalidExportKind, Span)> =
916                FxIndexMap::default();
917
918            let mut collect_possibly_invalid_exports =
919                |export_name: &Atom, span: &Span| match &**export_name {
920                    "getServerSideProps" | "getStaticProps" => {
921                        possibly_invalid_exports
922                            .insert(export_name.clone(), (InvalidExportKind::General, *span));
923                    }
924                    "generateMetadata" | "metadata" => {
925                        possibly_invalid_exports
926                            .insert(export_name.clone(), (InvalidExportKind::Metadata, *span));
927                    }
928                    "runtime" => {
929                        if self.cache_components_enabled {
930                            possibly_invalid_exports.insert(
931                                export_name.clone(),
932                                (
933                                    InvalidExportKind::RouteSegmentConfig(
934                                        NextConfigProperty::CacheComponents,
935                                    ),
936                                    *span,
937                                ),
938                            );
939                        } else if self.use_cache_enabled {
940                            possibly_invalid_exports.insert(
941                                export_name.clone(),
942                                (
943                                    InvalidExportKind::RouteSegmentConfig(
944                                        NextConfigProperty::UseCache,
945                                    ),
946                                    *span,
947                                ),
948                            );
949                        }
950                    }
951                    "dynamicParams" | "dynamic" | "fetchCache" | "revalidate"
952                    | "experimental_ppr"
953                        if self.cache_components_enabled =>
954                    {
955                        possibly_invalid_exports.insert(
956                            export_name.clone(),
957                            (
958                                InvalidExportKind::RouteSegmentConfig(
959                                    NextConfigProperty::CacheComponents,
960                                ),
961                                *span,
962                            ),
963                        );
964                    }
965                    "unstable_instant" if !self.cache_components_enabled => {
966                        possibly_invalid_exports.insert(
967                            export_name.clone(),
968                            (
969                                InvalidExportKind::RequiresRouteSegmentConfig(
970                                    NextConfigProperty::CacheComponents,
971                                ),
972                                *span,
973                            ),
974                        );
975                    }
976                    _ => (),
977                };
978
979            for export in &module.body {
980                match export {
981                    ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(export)) => {
982                        for specifier in &export.specifiers {
983                            if let ExportSpecifier::Named(named) = specifier {
984                                collect_possibly_invalid_exports(&named.orig.atom(), &named.span);
985                            }
986                        }
987                    }
988                    ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export)) => match &export.decl {
989                        Decl::Fn(f) => {
990                            collect_possibly_invalid_exports(&f.ident.sym, &f.ident.span);
991                        }
992                        Decl::Var(v) => {
993                            for decl in &v.decls {
994                                if let Pat::Ident(i) = &decl.name {
995                                    collect_possibly_invalid_exports(&i.sym, &i.span);
996                                }
997                            }
998                        }
999                        _ => {}
1000                    },
1001                    _ => {}
1002                }
1003            }
1004
1005            for (export_name, (kind, span)) in &possibly_invalid_exports {
1006                match kind {
1007                    InvalidExportKind::RouteSegmentConfig(property) => {
1008                        report_error(
1009                            &self.app_dir,
1010                            &self.filepath,
1011                            RSCErrorKind::NextRscErrIncompatibleRouteSegmentConfig(
1012                                *span,
1013                                export_name.to_string(),
1014                                *property,
1015                            ),
1016                        );
1017                    }
1018                    InvalidExportKind::RequiresRouteSegmentConfig(property) => {
1019                        report_error(
1020                            &self.app_dir,
1021                            &self.filepath,
1022                            RSCErrorKind::NextRscErrRequiresRouteSegmentConfig(
1023                                *span,
1024                                export_name.to_string(),
1025                                *property,
1026                            ),
1027                        );
1028                    }
1029                    InvalidExportKind::Metadata => {
1030                        // Client entry can't export `generateMetadata` or `metadata`.
1031                        if is_client_entry
1032                            && (export_name == "generateMetadata" || export_name == "metadata")
1033                        {
1034                            report_error(
1035                                &self.app_dir,
1036                                &self.filepath,
1037                                RSCErrorKind::NextRscErrClientMetadataExport((
1038                                    export_name.to_string(),
1039                                    *span,
1040                                )),
1041                            );
1042                        }
1043                        // Server entry can't export `generateMetadata` and `metadata` together,
1044                        // which is handled separately below.
1045                    }
1046                    InvalidExportKind::General => {
1047                        report_error(
1048                            &self.app_dir,
1049                            &self.filepath,
1050                            RSCErrorKind::NextRscErrInvalidApi((export_name.to_string(), *span)),
1051                        );
1052                    }
1053                }
1054            }
1055
1056            // Server entry can't export `generateMetadata` and `metadata` together.
1057            if !is_client_entry {
1058                let export1 = possibly_invalid_exports.get(&atom!("generateMetadata"));
1059                let export2 = possibly_invalid_exports.get(&atom!("metadata"));
1060
1061                if let (Some((_, span1)), Some((_, span2))) = (export1, export2) {
1062                    report_error(
1063                        &self.app_dir,
1064                        &self.filepath,
1065                        RSCErrorKind::NextRscErrConflictMetadataExport((*span1, *span2)),
1066                    );
1067                }
1068            }
1069        }
1070    }
1071
1072    /// ```js
1073    /// import dynamic from 'next/dynamic'
1074    ///
1075    /// dynamic(() => import(...)) // ✅
1076    /// dynamic(() => import(...), { ssr: true }) // ✅
1077    /// dynamic(() => import(...), { ssr: false }) // ❌
1078    /// ```
1079    fn check_for_next_ssr_false(&self, node: &CallExpr) -> Option<()> {
1080        if !self.is_callee_next_dynamic(&node.callee) {
1081            return None;
1082        }
1083
1084        let ssr_arg = node.args.get(1)?;
1085        let obj = ssr_arg.expr.as_object()?;
1086
1087        for prop in obj.props.iter().filter_map(|v| v.as_prop()?.as_key_value()) {
1088            let is_ssr = match &prop.key {
1089                PropName::Ident(IdentName { sym, .. }) => sym == "ssr",
1090                PropName::Str(s) => s.value == "ssr",
1091                _ => false,
1092            };
1093
1094            if is_ssr {
1095                let value = prop.value.as_lit()?;
1096                if let Lit::Bool(Bool { value: false, .. }) = value {
1097                    report_error(
1098                        &self.app_dir,
1099                        &self.filepath,
1100                        RSCErrorKind::NextSsrDynamicFalseNotAllowed(node.span),
1101                    );
1102                }
1103            }
1104        }
1105
1106        None
1107    }
1108}
1109
1110impl Visit for ReactServerComponentValidator {
1111    noop_visit_type!();
1112
1113    // coerce parsed script to run validation for the context, which is still
1114    // required even if file is empty
1115    fn visit_script(&mut self, script: &swc_core::ecma::ast::Script) {
1116        if script.body.is_empty() {
1117            self.visit_module(&Module::dummy());
1118        }
1119    }
1120
1121    fn visit_call_expr(&mut self, node: &CallExpr) {
1122        node.visit_children_with(self);
1123
1124        if self.is_react_server_layer {
1125            self.check_for_next_ssr_false(node);
1126        }
1127    }
1128
1129    fn visit_module(&mut self, module: &Module) {
1130        self.imports = ImportMap::analyze(module);
1131
1132        let (directive, imports, export_names) =
1133            collect_module_info(&self.app_dir, &self.filepath, module);
1134        let imports = Rc::new(imports);
1135
1136        self.module_directive = directive;
1137        self.export_names = export_names;
1138
1139        // Check for taint API usage without config (runs for all files)
1140        self.assert_react_taint_apis(&imports);
1141
1142        if self.is_react_server_layer {
1143            if directive == Some(ModuleDirective::UseClient) {
1144                return;
1145            } else {
1146                // Only assert server graph if file's bundle target is "server", e.g.
1147                // * server components pages
1148                // * pages bundles on SSR layer
1149                // * middleware
1150                // * app/pages api routes
1151                self.assert_server_graph(&imports, module);
1152            }
1153        } else {
1154            // Only assert client graph if the file is not an action or cache file,
1155            // and bundle target is "client" e.g.
1156            // * client components pages
1157            // * pages bundles on browser layer
1158            if directive != Some(ModuleDirective::UseServer)
1159                && directive != Some(ModuleDirective::UseCache)
1160            {
1161                self.assert_client_graph(&imports);
1162                self.assert_invalid_api(module, true);
1163            }
1164        }
1165
1166        module.visit_children_with(self);
1167    }
1168}
1169
1170/// Returns a visitor to assert react server components without any transform.
1171/// This is for the Turbopack which have its own transform phase for the server
1172/// components proxy.
1173///
1174/// This also returns a visitor instead of fold and performs better than running
1175/// whole transform as a folder.
1176pub fn server_components_assert(
1177    filename: FileName,
1178    config: Config,
1179    app_dir: Option<PathBuf>,
1180) -> impl Visit {
1181    let is_react_server_layer: bool = match &config {
1182        Config::WithOptions(x) => x.is_react_server_layer,
1183        _ => false,
1184    };
1185    let cache_components_enabled: bool = match &config {
1186        Config::WithOptions(x) => x.cache_components_enabled,
1187        _ => false,
1188    };
1189    let use_cache_enabled: bool = match &config {
1190        Config::WithOptions(x) => x.use_cache_enabled,
1191        _ => false,
1192    };
1193    let taint_enabled: bool = match &config {
1194        Config::WithOptions(x) => x.taint_enabled,
1195        _ => false,
1196    };
1197    let page_extensions: Vec<String> = match &config {
1198        Config::WithOptions(x) => x.page_extensions.clone(),
1199        _ => vec![],
1200    };
1201    let filename = match filename {
1202        FileName::Custom(path) => format!("<{path}>"),
1203        _ => filename.to_string(),
1204    };
1205    ReactServerComponentValidator::new(
1206        is_react_server_layer,
1207        cache_components_enabled,
1208        use_cache_enabled,
1209        taint_enabled,
1210        filename,
1211        app_dir,
1212        page_extensions,
1213    )
1214}
1215
1216/// Runs react server component transform for the module proxy, as well as
1217/// running assertion.
1218pub fn server_components<C: Comments>(
1219    filename: Arc<FileName>,
1220    config: Config,
1221    comments: C,
1222    app_dir: Option<PathBuf>,
1223) -> impl Pass + VisitMut {
1224    let is_react_server_layer: bool = match &config {
1225        Config::WithOptions(x) => x.is_react_server_layer,
1226        _ => false,
1227    };
1228    let cache_components_enabled: bool = match &config {
1229        Config::WithOptions(x) => x.cache_components_enabled,
1230        _ => false,
1231    };
1232    let use_cache_enabled: bool = match &config {
1233        Config::WithOptions(x) => x.use_cache_enabled,
1234        _ => false,
1235    };
1236    let taint_enabled: bool = match &config {
1237        Config::WithOptions(x) => x.taint_enabled,
1238        _ => false,
1239    };
1240    let page_extensions: Vec<String> = match &config {
1241        Config::WithOptions(x) => x.page_extensions.clone(),
1242        _ => vec![],
1243    };
1244    visit_mut_pass(ReactServerComponents {
1245        is_react_server_layer,
1246        cache_components_enabled,
1247        use_cache_enabled,
1248        taint_enabled,
1249        comments,
1250        filepath: match &*filename {
1251            FileName::Custom(path) => format!("<{path}>"),
1252            _ => filename.to_string(),
1253        },
1254        app_dir,
1255        page_extensions,
1256    })
1257}