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                    {
956                        possibly_invalid_exports.insert(
957                            export_name.clone(),
958                            (
959                                InvalidExportKind::RouteSegmentConfig(
960                                    NextConfigProperty::CacheComponents,
961                                ),
962                                *span,
963                            ),
964                        );
965                    }
966                    "unstable_instant" if !self.cache_components_enabled => {
967                        possibly_invalid_exports.insert(
968                            export_name.clone(),
969                            (
970                                InvalidExportKind::RequiresRouteSegmentConfig(
971                                    NextConfigProperty::CacheComponents,
972                                ),
973                                *span,
974                            ),
975                        );
976                    }
977                    _ => (),
978                };
979
980            for export in &module.body {
981                match export {
982                    ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(export)) => {
983                        for specifier in &export.specifiers {
984                            if let ExportSpecifier::Named(named) = specifier {
985                                collect_possibly_invalid_exports(&named.orig.atom(), &named.span);
986                            }
987                        }
988                    }
989                    ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export)) => match &export.decl {
990                        Decl::Fn(f) => {
991                            collect_possibly_invalid_exports(&f.ident.sym, &f.ident.span);
992                        }
993                        Decl::Var(v) => {
994                            for decl in &v.decls {
995                                if let Pat::Ident(i) = &decl.name {
996                                    collect_possibly_invalid_exports(&i.sym, &i.span);
997                                }
998                            }
999                        }
1000                        _ => {}
1001                    },
1002                    _ => {}
1003                }
1004            }
1005
1006            for (export_name, (kind, span)) in &possibly_invalid_exports {
1007                match kind {
1008                    InvalidExportKind::RouteSegmentConfig(property) => {
1009                        report_error(
1010                            &self.app_dir,
1011                            &self.filepath,
1012                            RSCErrorKind::NextRscErrIncompatibleRouteSegmentConfig(
1013                                *span,
1014                                export_name.to_string(),
1015                                *property,
1016                            ),
1017                        );
1018                    }
1019                    InvalidExportKind::RequiresRouteSegmentConfig(property) => {
1020                        report_error(
1021                            &self.app_dir,
1022                            &self.filepath,
1023                            RSCErrorKind::NextRscErrRequiresRouteSegmentConfig(
1024                                *span,
1025                                export_name.to_string(),
1026                                *property,
1027                            ),
1028                        );
1029                    }
1030                    InvalidExportKind::Metadata => {
1031                        // Client entry can't export `generateMetadata` or `metadata`.
1032                        if is_client_entry
1033                            && (export_name == "generateMetadata" || export_name == "metadata")
1034                        {
1035                            report_error(
1036                                &self.app_dir,
1037                                &self.filepath,
1038                                RSCErrorKind::NextRscErrClientMetadataExport((
1039                                    export_name.to_string(),
1040                                    *span,
1041                                )),
1042                            );
1043                        }
1044                        // Server entry can't export `generateMetadata` and `metadata` together,
1045                        // which is handled separately below.
1046                    }
1047                    InvalidExportKind::General => {
1048                        report_error(
1049                            &self.app_dir,
1050                            &self.filepath,
1051                            RSCErrorKind::NextRscErrInvalidApi((export_name.to_string(), *span)),
1052                        );
1053                    }
1054                }
1055            }
1056
1057            // Server entry can't export `generateMetadata` and `metadata` together.
1058            if !is_client_entry {
1059                let export1 = possibly_invalid_exports.get(&atom!("generateMetadata"));
1060                let export2 = possibly_invalid_exports.get(&atom!("metadata"));
1061
1062                if let (Some((_, span1)), Some((_, span2))) = (export1, export2) {
1063                    report_error(
1064                        &self.app_dir,
1065                        &self.filepath,
1066                        RSCErrorKind::NextRscErrConflictMetadataExport((*span1, *span2)),
1067                    );
1068                }
1069            }
1070        }
1071    }
1072
1073    /// ```js
1074    /// import dynamic from 'next/dynamic'
1075    ///
1076    /// dynamic(() => import(...)) // ✅
1077    /// dynamic(() => import(...), { ssr: true }) // ✅
1078    /// dynamic(() => import(...), { ssr: false }) // ❌
1079    /// ```
1080    fn check_for_next_ssr_false(&self, node: &CallExpr) -> Option<()> {
1081        if !self.is_callee_next_dynamic(&node.callee) {
1082            return None;
1083        }
1084
1085        let ssr_arg = node.args.get(1)?;
1086        let obj = ssr_arg.expr.as_object()?;
1087
1088        for prop in obj.props.iter().filter_map(|v| v.as_prop()?.as_key_value()) {
1089            let is_ssr = match &prop.key {
1090                PropName::Ident(IdentName { sym, .. }) => sym == "ssr",
1091                PropName::Str(s) => s.value == "ssr",
1092                _ => false,
1093            };
1094
1095            if is_ssr {
1096                let value = prop.value.as_lit()?;
1097                if let Lit::Bool(Bool { value: false, .. }) = value {
1098                    report_error(
1099                        &self.app_dir,
1100                        &self.filepath,
1101                        RSCErrorKind::NextSsrDynamicFalseNotAllowed(node.span),
1102                    );
1103                }
1104            }
1105        }
1106
1107        None
1108    }
1109}
1110
1111impl Visit for ReactServerComponentValidator {
1112    noop_visit_type!();
1113
1114    // coerce parsed script to run validation for the context, which is still
1115    // required even if file is empty
1116    fn visit_script(&mut self, script: &swc_core::ecma::ast::Script) {
1117        if script.body.is_empty() {
1118            self.visit_module(&Module::dummy());
1119        }
1120    }
1121
1122    fn visit_call_expr(&mut self, node: &CallExpr) {
1123        node.visit_children_with(self);
1124
1125        if self.is_react_server_layer {
1126            self.check_for_next_ssr_false(node);
1127        }
1128    }
1129
1130    fn visit_module(&mut self, module: &Module) {
1131        self.imports = ImportMap::analyze(module);
1132
1133        let (directive, imports, export_names) =
1134            collect_module_info(&self.app_dir, &self.filepath, module);
1135        let imports = Rc::new(imports);
1136
1137        self.module_directive = directive;
1138        self.export_names = export_names;
1139
1140        // Check for taint API usage without config (runs for all files)
1141        self.assert_react_taint_apis(&imports);
1142
1143        if self.is_react_server_layer {
1144            if directive == Some(ModuleDirective::UseClient) {
1145                return;
1146            } else {
1147                // Only assert server graph if file's bundle target is "server", e.g.
1148                // * server components pages
1149                // * pages bundles on SSR layer
1150                // * middleware
1151                // * app/pages api routes
1152                self.assert_server_graph(&imports, module);
1153            }
1154        } else {
1155            // Only assert client graph if the file is not an action or cache file,
1156            // and bundle target is "client" e.g.
1157            // * client components pages
1158            // * pages bundles on browser layer
1159            if directive != Some(ModuleDirective::UseServer)
1160                && directive != Some(ModuleDirective::UseCache)
1161            {
1162                self.assert_client_graph(&imports);
1163                self.assert_invalid_api(module, true);
1164            }
1165        }
1166
1167        module.visit_children_with(self);
1168    }
1169}
1170
1171/// Returns a visitor to assert react server components without any transform.
1172/// This is for the Turbopack which have its own transform phase for the server
1173/// components proxy.
1174///
1175/// This also returns a visitor instead of fold and performs better than running
1176/// whole transform as a folder.
1177pub fn server_components_assert(
1178    filename: FileName,
1179    config: Config,
1180    app_dir: Option<PathBuf>,
1181) -> impl Visit {
1182    let is_react_server_layer: bool = match &config {
1183        Config::WithOptions(x) => x.is_react_server_layer,
1184        _ => false,
1185    };
1186    let cache_components_enabled: bool = match &config {
1187        Config::WithOptions(x) => x.cache_components_enabled,
1188        _ => false,
1189    };
1190    let use_cache_enabled: bool = match &config {
1191        Config::WithOptions(x) => x.use_cache_enabled,
1192        _ => false,
1193    };
1194    let taint_enabled: bool = match &config {
1195        Config::WithOptions(x) => x.taint_enabled,
1196        _ => false,
1197    };
1198    let page_extensions: Vec<String> = match &config {
1199        Config::WithOptions(x) => x.page_extensions.clone(),
1200        _ => vec![],
1201    };
1202    let filename = match filename {
1203        FileName::Custom(path) => format!("<{path}>"),
1204        _ => filename.to_string(),
1205    };
1206    ReactServerComponentValidator::new(
1207        is_react_server_layer,
1208        cache_components_enabled,
1209        use_cache_enabled,
1210        taint_enabled,
1211        filename,
1212        app_dir,
1213        page_extensions,
1214    )
1215}
1216
1217/// Runs react server component transform for the module proxy, as well as
1218/// running assertion.
1219pub fn server_components<C: Comments>(
1220    filename: Arc<FileName>,
1221    config: Config,
1222    comments: C,
1223    app_dir: Option<PathBuf>,
1224) -> impl Pass + VisitMut {
1225    let is_react_server_layer: bool = match &config {
1226        Config::WithOptions(x) => x.is_react_server_layer,
1227        _ => false,
1228    };
1229    let cache_components_enabled: bool = match &config {
1230        Config::WithOptions(x) => x.cache_components_enabled,
1231        _ => false,
1232    };
1233    let use_cache_enabled: bool = match &config {
1234        Config::WithOptions(x) => x.use_cache_enabled,
1235        _ => false,
1236    };
1237    let taint_enabled: bool = match &config {
1238        Config::WithOptions(x) => x.taint_enabled,
1239        _ => false,
1240    };
1241    let page_extensions: Vec<String> = match &config {
1242        Config::WithOptions(x) => x.page_extensions.clone(),
1243        _ => vec![],
1244    };
1245    visit_mut_pass(ReactServerComponents {
1246        is_react_server_layer,
1247        cache_components_enabled,
1248        use_cache_enabled,
1249        taint_enabled,
1250        comments,
1251        filepath: match &*filename {
1252            FileName::Custom(path) => format!("<{path}>"),
1253            _ => filename.to_string(),
1254        },
1255        app_dir,
1256        page_extensions,
1257    })
1258}