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