Skip to main content

next_custom_transforms/transforms/
react_server_components.rs

1use std::{
2    fmt::{self, Display},
3    iter::FromIterator,
4    path::PathBuf,
5    rc::Rc,
6    sync::{Arc, LazyLock},
7};
8
9use regex::Regex;
10use rustc_hash::FxHashMap;
11use serde::Deserialize;
12
13fn build_page_extensions_regex(page_extensions: &[String]) -> String {
14    if page_extensions.is_empty() {
15        "(ts|js)x?".to_string()
16    } else {
17        let escaped: Vec<String> = page_extensions
18            .iter()
19            .map(|ext| regex::escape(ext))
20            .collect();
21        format!("({})", escaped.join("|"))
22    }
23}
24use swc_core::{
25    atoms::{Atom, Wtf8Atom, atom},
26    common::{
27        DUMMY_SP, FileName, Span, Spanned,
28        comments::{Comment, CommentKind, Comments},
29        errors::HANDLER,
30        util::take::Take,
31    },
32    ecma::{
33        ast::*,
34        utils::{ExprFactory, prepend_stmts, prop_name_eq, quote_ident, quote_str},
35        visit::{
36            Visit, VisitMut, VisitMutWith, VisitWith, noop_visit_mut_type, noop_visit_type,
37            visit_mut_pass,
38        },
39    },
40};
41
42use super::{cjs_finder::contains_cjs, import_analyzer::ImportMap};
43use crate::FxIndexMap;
44
45#[derive(Clone, Debug, Deserialize)]
46#[serde(untagged)]
47pub enum Config {
48    All(bool),
49    WithOptions(Options),
50}
51
52impl Config {
53    pub fn truthy(&self) -> bool {
54        match self {
55            Config::All(b) => *b,
56            Config::WithOptions(_) => true,
57        }
58    }
59}
60
61#[derive(Clone, Debug, Deserialize)]
62#[serde(rename_all = "camelCase")]
63pub struct Options {
64    pub is_react_server_layer: bool,
65    pub cache_components_enabled: bool,
66    pub use_cache_enabled: bool,
67    #[serde(default)]
68    pub taint_enabled: bool,
69    #[serde(default)]
70    pub page_extensions: Vec<String>,
71}
72
73/// A visitor that transforms given module to use module proxy if it's a React
74/// server component.
75/// **NOTE** Turbopack uses ClientDirectiveTransformer for the
76/// same purpose, so does not run this transform.
77struct ReactServerComponents<C: Comments> {
78    is_react_server_layer: bool,
79    cache_components_enabled: bool,
80    use_cache_enabled: bool,
81    taint_enabled: bool,
82    filepath: String,
83    app_dir: Option<PathBuf>,
84    comments: C,
85    page_extensions: Vec<String>,
86}
87
88#[derive(Clone, Debug)]
89struct ModuleImports {
90    source: (Wtf8Atom, Span),
91    specifiers: Vec<(Atom, Span)>,
92}
93
94#[allow(clippy::enum_variant_names)]
95#[derive(Clone, Copy, Debug, PartialEq, Eq)]
96enum ModuleDirective {
97    UseClient,
98    UseServer,
99    UseCache,
100}
101
102enum RSCErrorKind {
103    UseClientWithUseServer(Span),
104    UseClientWithUseCache(Span),
105    NextRscErrServerImport((String, Span)),
106    NextRscErrClientImport((String, Span)),
107    NextRscErrClientDirective(Span),
108    NextRscErrReactApi((String, Span)),
109    NextRscErrErrorFileServerComponent(Span),
110    NextRscErrClientMetadataExport((String, Span)),
111    NextRscErrConflictMetadataExport((Span, Span)),
112    NextRscErrInvalidApi((String, Span)),
113    NextRscErrDeprecatedApi((String, String, Span)),
114    NextSsrDynamicFalseNotAllowed(Span),
115    NextRscErrIncompatibleRouteSegmentConfig(Span, String, NextConfigProperty),
116    NextRscErrRequiresRouteSegmentConfig(Span, String, NextConfigProperty),
117    NextRscErrTaintWithoutConfig((String, Span)),
118}
119
120#[derive(Clone, Debug, Copy)]
121enum NextConfigProperty {
122    CacheComponents,
123    UseCache,
124}
125
126impl Display for NextConfigProperty {
127    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128        match self {
129            NextConfigProperty::CacheComponents => write!(f, "cacheComponents"),
130            NextConfigProperty::UseCache => write!(f, "experimental.useCache"),
131        }
132    }
133}
134
135enum InvalidExportKind {
136    General,
137    Metadata,
138    RouteSegmentConfig(NextConfigProperty),
139    RequiresRouteSegmentConfig(NextConfigProperty),
140}
141
142impl<C: Comments> VisitMut for ReactServerComponents<C> {
143    noop_visit_mut_type!();
144
145    fn visit_mut_module(&mut self, module: &mut Module) {
146        // Run the validator first to assert, collect directives and imports.
147        let mut validator = ReactServerComponentValidator::new(
148            self.is_react_server_layer,
149            self.cache_components_enabled,
150            self.use_cache_enabled,
151            self.taint_enabled,
152            self.filepath.clone(),
153            self.app_dir.clone(),
154            self.page_extensions.clone(),
155        );
156
157        module.visit_with(&mut validator);
158
159        let is_client_entry = validator.module_directive == Some(ModuleDirective::UseClient);
160        let export_names = validator.export_names;
161
162        self.remove_top_level_directive(module);
163
164        let is_cjs = contains_cjs(module);
165
166        if self.is_react_server_layer {
167            if is_client_entry {
168                self.to_module_ref(module, is_cjs, &export_names);
169                return;
170            }
171        } else if is_client_entry {
172            self.prepend_comment_node(module, is_cjs, &export_names);
173        }
174        module.visit_mut_children_with(self)
175    }
176}
177
178impl<C: Comments> ReactServerComponents<C> {
179    /// removes specific directive from the AST.
180    fn remove_top_level_directive(&mut self, module: &mut Module) {
181        module.body.retain(|item| {
182            if let ModuleItem::Stmt(stmt) = item
183                && let Some(expr_stmt) = stmt.as_expr()
184                && let Expr::Lit(Lit::Str(Str { value, .. })) = &*expr_stmt.expr
185                && &**value == "use client"
186            {
187                // Remove the directive.
188                return false;
189            }
190            true
191        });
192    }
193
194    // Convert the client module to the module reference code and add a special
195    // comment to the top of the file.
196    fn to_module_ref(&self, module: &mut Module, is_cjs: bool, export_names: &[Atom]) {
197        // Clear all the statements and module declarations.
198        module.body.clear();
199
200        let proxy_ident = quote_ident!("createProxy");
201        let filepath = quote_str!(&*self.filepath);
202
203        prepend_stmts(
204            &mut module.body,
205            vec![
206                ModuleItem::Stmt(Stmt::Decl(Decl::Var(Box::new(VarDecl {
207                    span: DUMMY_SP,
208                    kind: VarDeclKind::Const,
209                    decls: vec![VarDeclarator {
210                        span: DUMMY_SP,
211                        name: Pat::Object(ObjectPat {
212                            span: DUMMY_SP,
213                            props: vec![ObjectPatProp::Assign(AssignPatProp {
214                                span: DUMMY_SP,
215                                key: proxy_ident.into(),
216                                value: None,
217                            })],
218                            optional: false,
219                            type_ann: None,
220                        }),
221                        init: Some(Box::new(Expr::Call(CallExpr {
222                            span: DUMMY_SP,
223                            callee: quote_ident!("require").as_callee(),
224                            args: vec![quote_str!("private-next-rsc-mod-ref-proxy").as_arg()],
225                            ..Default::default()
226                        }))),
227                        definite: false,
228                    }],
229                    ..Default::default()
230                })))),
231                ModuleItem::Stmt(Stmt::Expr(ExprStmt {
232                    span: DUMMY_SP,
233                    expr: Box::new(Expr::Assign(AssignExpr {
234                        span: DUMMY_SP,
235                        left: MemberExpr {
236                            span: DUMMY_SP,
237                            obj: Box::new(Expr::Ident(quote_ident!("module").into())),
238                            prop: MemberProp::Ident(quote_ident!("exports")),
239                        }
240                        .into(),
241                        op: op!("="),
242                        right: Box::new(Expr::Call(CallExpr {
243                            span: DUMMY_SP,
244                            callee: quote_ident!("createProxy").as_callee(),
245                            args: vec![filepath.as_arg()],
246                            ..Default::default()
247                        })),
248                    })),
249                })),
250            ]
251            .into_iter(),
252        );
253
254        self.prepend_comment_node(module, is_cjs, export_names);
255    }
256
257    fn prepend_comment_node(&self, module: &Module, is_cjs: bool, export_names: &[Atom]) {
258        // Prepend a special comment to the top of the file that contains
259        // module export names and the detected module type.
260        self.comments.add_leading(
261            module.span.lo,
262            Comment {
263                span: DUMMY_SP,
264                kind: CommentKind::Block,
265                text: format!(
266                    " __next_internal_client_entry_do_not_use__ {} {} ",
267                    join_atoms(export_names),
268                    if is_cjs { "cjs" } else { "auto" }
269                )
270                .into(),
271            },
272        );
273    }
274}
275
276fn join_atoms(atoms: &[Atom]) -> String {
277    atoms
278        .iter()
279        .map(|atom| atom.as_ref())
280        .collect::<Vec<_>>()
281        .join(",")
282}
283
284/// Returns whether `filepath` is a file inside `app_dir`.
285///
286/// The App Router file conventions are matched by filename. The same filenames
287/// are also valid Pages Router routes, where the conventions do not apply.
288/// `pages/sitemap.js` is an ordinary page, not a sitemap. Gate App Router
289/// checks on this function to keep them out of the Pages Router.
290fn is_in_app_dir(app_dir: &Option<PathBuf>, filepath: &str) -> bool {
291    let Some(app_dir) = app_dir.as_ref().and_then(|app_dir| app_dir.to_str()) else {
292        return false;
293    };
294
295    // The rest of the path must start with a separator. A plain prefix match
296    // would also accept a sibling directory such as `apparel` for `app`.
297    filepath
298        .strip_prefix(app_dir.trim_end_matches(['/', '\\']))
299        .is_some_and(|rest| rest.starts_with(['/', '\\']))
300}
301
302/// Consolidated place to parse, generate error messages for the RSC parsing
303/// errors.
304fn report_error(app_dir: &Option<PathBuf>, filepath: &str, error_kind: RSCErrorKind) {
305    let (msg, spans) = match error_kind {
306        RSCErrorKind::UseClientWithUseServer(span) => (
307            "It's not possible to have both \"use client\" and \"use server\" directives in the \
308             same file."
309                .to_string(),
310            vec![span],
311        ),
312        RSCErrorKind::UseClientWithUseCache(span) => (
313            "It's not possible to have both \"use client\" and \"use cache\" directives in the \
314             same file."
315                .to_string(),
316            vec![span],
317        ),
318        RSCErrorKind::NextRscErrClientDirective(span) => (
319            "The \"use client\" directive must be placed before other expressions. Move it to \
320             the top of the file to resolve this issue."
321                .to_string(),
322            vec![span],
323        ),
324        RSCErrorKind::NextRscErrServerImport((source, span)) => {
325            let msg = match source.as_str() {
326                // If importing "react-dom/server", we should show a different error.
327                "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(),
328                // If importing "next/router", we should tell them to use "next/navigation".
329                "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(),
330                _ => 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")
331            };
332
333            (msg, vec![span])
334        }
335        RSCErrorKind::NextRscErrClientImport((source, span)) => {
336            let msg = if !is_in_app_dir(app_dir, filepath) {
337                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")
338            } else {
339                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")
340            };
341            (msg, vec![span])
342        }
343        RSCErrorKind::NextRscErrReactApi((source, span)) => {
344            let msg = if source == "Component" {
345                "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()
346            } else {
347                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")
348            };
349
350            (msg, vec![span])
351        },
352        RSCErrorKind::NextRscErrErrorFileServerComponent(span) => {
353            (
354                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"),
355                vec![span]
356            )
357        },
358        RSCErrorKind::NextRscErrClientMetadataExport((source, span)) => {
359            (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])
360        },
361        RSCErrorKind::NextRscErrConflictMetadataExport((span1, span2)) => (
362            "\"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(),
363            vec![span1, span2]
364        ),
365        RSCErrorKind::NextRscErrInvalidApi((source, span)) => (
366            format!("\"{source}\" is not supported in app/. Read more: https://nextjs.org/docs/app/building-your-application/data-fetching\n\n"), vec![span]
367        ),
368        RSCErrorKind::NextRscErrDeprecatedApi((source, item, span)) => match (&*source, &*item) {
369            ("next/server", "ImageResponse") => (
370                "ImageResponse moved from \"next/server\" to \"next/og\" since Next.js 14, please \
371                 import from \"next/og\" instead"
372                    .to_string(),
373                vec![span],
374            ),
375            _ => (format!("\"{source}\" is deprecated."), vec![span]),
376        },
377        RSCErrorKind::NextSsrDynamicFalseNotAllowed(span) => (
378            "`ssr: false` is not allowed with `next/dynamic` in Server Components. Please move it into a Client Component."
379                .to_string(),
380            vec![span],
381        ),
382        RSCErrorKind::NextRscErrIncompatibleRouteSegmentConfig(span, segment, property) => (
383            format!("Route segment config \"{segment}\" is not compatible with `nextConfig.{property}`. Please remove it."),
384            vec![span],
385        ),
386        RSCErrorKind::NextRscErrRequiresRouteSegmentConfig(span, segment, property) => (
387            format!("Route segment config \"{segment}\" requires `nextConfig.{property}` to be enabled."),
388            vec![span],
389        ),
390        RSCErrorKind::NextRscErrTaintWithoutConfig((api_name, span)) => (
391            format!(
392                "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"
393            ),
394            vec![span],
395        ),
396    };
397
398    HANDLER.with(|handler| handler.struct_span_err(spans, msg.as_str()).emit())
399}
400
401/// Collects module directive, imports, and exports from top-level statements
402fn collect_module_info(
403    app_dir: &Option<PathBuf>,
404    filepath: &str,
405    module: &Module,
406) -> (Option<ModuleDirective>, Vec<ModuleImports>, Vec<Atom>) {
407    let mut imports: Vec<ModuleImports> = vec![];
408    let mut finished_directives = false;
409    let mut is_client_entry = false;
410    let mut is_action_file = false;
411    let mut is_cache_file = false;
412
413    let mut export_names = vec![];
414
415    let _ = &module.body.iter().for_each(|item| {
416        match item {
417            ModuleItem::Stmt(stmt) => {
418                if !stmt.is_expr() {
419                    // Not an expression.
420                    finished_directives = true;
421                }
422
423                match stmt.as_expr() {
424                    Some(expr_stmt) => {
425                        match &*expr_stmt.expr {
426                            Expr::Lit(Lit::Str(Str { value, .. })) => {
427                                if &**value == "use client" {
428                                    if !finished_directives {
429                                        is_client_entry = true;
430
431                                        if is_action_file {
432                                            report_error(
433                                                app_dir,
434                                                filepath,
435                                                RSCErrorKind::UseClientWithUseServer(
436                                                    expr_stmt.span,
437                                                ),
438                                            );
439                                        } else if is_cache_file {
440                                            report_error(
441                                                app_dir,
442                                                filepath,
443                                                RSCErrorKind::UseClientWithUseCache(expr_stmt.span),
444                                            );
445                                        }
446                                    } else {
447                                        report_error(
448                                            app_dir,
449                                            filepath,
450                                            RSCErrorKind::NextRscErrClientDirective(expr_stmt.span),
451                                        );
452                                    }
453                                } else if &**value == "use server" && !finished_directives {
454                                    is_action_file = true;
455
456                                    if is_client_entry {
457                                        report_error(
458                                            app_dir,
459                                            filepath,
460                                            RSCErrorKind::UseClientWithUseServer(expr_stmt.span),
461                                        );
462                                    }
463                                } else if (&**value == "use cache"
464                                    || value.starts_with("use cache: "))
465                                    && !finished_directives
466                                {
467                                    is_cache_file = true;
468
469                                    if is_client_entry {
470                                        report_error(
471                                            app_dir,
472                                            filepath,
473                                            RSCErrorKind::UseClientWithUseCache(expr_stmt.span),
474                                        );
475                                    }
476                                }
477                            }
478                            // Match `ParenthesisExpression` which is some formatting tools
479                            // usually do: ('use client'). In these case we need to throw
480                            // an exception because they are not valid directives.
481                            Expr::Paren(ParenExpr { expr, .. }) => {
482                                finished_directives = true;
483                                if let Expr::Lit(Lit::Str(Str { value, .. })) = &**expr
484                                    && &**value == "use client"
485                                {
486                                    report_error(
487                                        app_dir,
488                                        filepath,
489                                        RSCErrorKind::NextRscErrClientDirective(expr_stmt.span),
490                                    );
491                                }
492                            }
493                            _ => {
494                                // Other expression types.
495                                finished_directives = true;
496                            }
497                        }
498                    }
499                    None => {
500                        // Not an expression.
501                        finished_directives = true;
502                    }
503                }
504            }
505            ModuleItem::ModuleDecl(ModuleDecl::Import(
506                import @ ImportDecl {
507                    type_only: false, ..
508                },
509            )) => {
510                let source = import.src.value.clone();
511                let specifiers = import
512                    .specifiers
513                    .iter()
514                    .filter(|specifier| {
515                        !matches!(
516                            specifier,
517                            ImportSpecifier::Named(ImportNamedSpecifier {
518                                is_type_only: true,
519                                ..
520                            })
521                        )
522                    })
523                    .map(|specifier| match specifier {
524                        ImportSpecifier::Named(named) => match &named.imported {
525                            Some(imported) => (imported.atom().into_owned(), imported.span()),
526                            None => (named.local.to_id().0, named.local.span),
527                        },
528                        ImportSpecifier::Default(d) => (atom!(""), d.span),
529                        ImportSpecifier::Namespace(n) => (atom!("*"), n.span),
530                    })
531                    .collect();
532
533                imports.push(ModuleImports {
534                    source: (source, import.span),
535                    specifiers,
536                });
537
538                finished_directives = true;
539            }
540            // Collect all export names.
541            ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(e)) => {
542                for specifier in &e.specifiers {
543                    export_names.push(match specifier {
544                        ExportSpecifier::Default(_) => atom!("default"),
545                        ExportSpecifier::Namespace(_) => atom!("*"),
546                        ExportSpecifier::Named(named) => named
547                            .exported
548                            .as_ref()
549                            .unwrap_or(&named.orig)
550                            .atom()
551                            .into_owned(),
552                    })
553                }
554                finished_directives = true;
555            }
556            ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl { decl, .. })) => {
557                match decl {
558                    Decl::Class(ClassDecl { ident, .. }) => {
559                        export_names.push(ident.sym.clone());
560                    }
561                    Decl::Fn(FnDecl { ident, .. }) => {
562                        export_names.push(ident.sym.clone());
563                    }
564                    Decl::Var(var) => {
565                        for decl in &var.decls {
566                            if let Pat::Ident(ident) = &decl.name {
567                                export_names.push(ident.id.sym.clone());
568                            }
569                        }
570                    }
571                    _ => {}
572                }
573                finished_directives = true;
574            }
575            ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(ExportDefaultDecl { .. })) => {
576                export_names.push(atom!("default"));
577                finished_directives = true;
578            }
579            ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(ExportDefaultExpr { .. })) => {
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!["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_navigation",
729                        "unstable_prefetch",
730                        // "unstable_noStore" // no-op in client, but allowed for legacy reasons
731                    ],
732                ),
733            ]),
734            react_taint_apis: vec![
735                "experimental_taintObjectReference",
736                "experimental_taintUniqueValue",
737            ],
738            imports: ImportMap::default(),
739            page_extensions,
740        }
741    }
742
743    fn is_from_node_modules(&self, filepath: &str) -> bool {
744        static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"node_modules[\\/]").unwrap());
745        RE.is_match(filepath)
746    }
747
748    fn is_callee_next_dynamic(&self, callee: &Callee) -> bool {
749        match callee {
750            Callee::Expr(expr) => self.imports.is_import(expr, "next/dynamic", "default"),
751            _ => false,
752        }
753    }
754
755    // Asserts the server lib apis
756    // e.g.
757    // assert_invalid_server_lib_apis("react", import)
758    // assert_invalid_server_lib_apis("react-dom", import)
759    fn assert_invalid_server_lib_apis(&self, import_source: &Wtf8Atom, import: &ModuleImports) {
760        let deprecated_apis = self.deprecated_apis_mapping.get(import_source);
761        if let Some(deprecated_apis) = deprecated_apis {
762            for specifier in &import.specifiers {
763                if deprecated_apis.contains(&specifier.0.as_str()) {
764                    report_error(
765                        &self.app_dir,
766                        &self.filepath,
767                        RSCErrorKind::NextRscErrDeprecatedApi((
768                            import_source.to_string_lossy().into_owned(),
769                            specifier.0.to_string(),
770                            specifier.1,
771                        )),
772                    );
773                }
774            }
775        }
776
777        let invalid_apis = self.invalid_server_lib_apis_mapping.get(import_source);
778        if let Some(invalid_apis) = invalid_apis {
779            for specifier in &import.specifiers {
780                if invalid_apis.contains(&specifier.0.as_str()) {
781                    report_error(
782                        &self.app_dir,
783                        &self.filepath,
784                        RSCErrorKind::NextRscErrReactApi((specifier.0.to_string(), specifier.1)),
785                    );
786                }
787            }
788        }
789    }
790
791    /// Check for React taint API imports when taint is not enabled
792    fn assert_react_taint_apis(&self, imports: &[ModuleImports]) {
793        // Skip check if taint is enabled or if file is from node_modules
794        if self.taint_enabled || self.is_from_node_modules(&self.filepath) {
795            return;
796        }
797
798        for import in imports {
799            let source = &import.source.0;
800            // Only check imports from 'react'
801            if source.as_str() != Some("react") {
802                continue;
803            }
804
805            for specifier in &import.specifiers {
806                if self.react_taint_apis.contains(&specifier.0.as_str()) {
807                    report_error(
808                        &self.app_dir,
809                        &self.filepath,
810                        RSCErrorKind::NextRscErrTaintWithoutConfig((
811                            specifier.0.to_string(),
812                            specifier.1,
813                        )),
814                    );
815                }
816            }
817        }
818    }
819
820    fn assert_server_graph(&self, imports: &[ModuleImports], module: &Module) {
821        // If the
822        if self.is_from_node_modules(&self.filepath) {
823            return;
824        }
825        for import in imports {
826            let source = &import.source.0;
827            if self.invalid_server_imports.contains(source) {
828                report_error(
829                    &self.app_dir,
830                    &self.filepath,
831                    RSCErrorKind::NextRscErrServerImport((
832                        source.to_string_lossy().into_owned(),
833                        import.source.1,
834                    )),
835                );
836            }
837
838            self.assert_invalid_server_lib_apis(source, import);
839        }
840
841        self.assert_invalid_api(module, false);
842        self.assert_server_filename(module);
843    }
844
845    fn assert_server_filename(&self, module: &Module) {
846        if self.is_from_node_modules(&self.filepath) {
847            return;
848        }
849        let ext_pattern = build_page_extensions_regex(&self.page_extensions);
850        let re = Regex::new(&format!(r"[\\/]((global-)?error)\.{ext_pattern}$")).unwrap();
851
852        let is_error_file = re.is_match(&self.filepath);
853
854        if is_error_file && is_in_app_dir(&self.app_dir, &self.filepath) {
855            let span = if let Some(first_item) = module.body.first() {
856                first_item.span()
857            } else {
858                module.span
859            };
860
861            report_error(
862                &self.app_dir,
863                &self.filepath,
864                RSCErrorKind::NextRscErrErrorFileServerComponent(span),
865            );
866        }
867    }
868
869    fn assert_client_graph(&self, imports: &[ModuleImports]) {
870        if self.is_from_node_modules(&self.filepath) {
871            return;
872        }
873        for import in imports {
874            let source = &import.source.0;
875
876            if self.invalid_client_imports.contains(source) {
877                report_error(
878                    &self.app_dir,
879                    &self.filepath,
880                    RSCErrorKind::NextRscErrClientImport((
881                        source.to_string_lossy().into_owned(),
882                        import.source.1,
883                    )),
884                );
885            }
886
887            let invalid_apis = self.invalid_client_lib_apis_mapping.get(source);
888            if let Some(invalid_apis) = invalid_apis {
889                for specifier in &import.specifiers {
890                    if invalid_apis.contains(&specifier.0.as_str()) {
891                        report_error(
892                            &self.app_dir,
893                            &self.filepath,
894                            RSCErrorKind::NextRscErrClientImport((
895                                specifier.0.to_string(),
896                                specifier.1,
897                            )),
898                        );
899                    }
900                }
901            }
902        }
903    }
904
905    fn assert_invalid_api(&self, module: &Module, is_client_entry: bool) {
906        if self.is_from_node_modules(&self.filepath) {
907            return;
908        }
909        let ext_pattern = build_page_extensions_regex(&self.page_extensions);
910        // Metadata convention files (e.g. `icon`, `opengraph-image`, `sitemap`)
911        // compile to route handlers and accept the same route segment configs,
912        // so they're subject to the same `cacheComponents`/`useCache`
913        // restrictions as `page`/`layout`/`route` entries.
914        let re = Regex::new(&format!(
915            r"[\\/](page|layout|route|icon\d?|apple-icon\d?|opengraph-image\d?|twitter-image\d?|sitemap|robots|manifest)\.{ext_pattern}$",
916        ))
917        .unwrap();
918        let is_app_entry =
919            re.is_match(&self.filepath) && is_in_app_dir(&self.app_dir, &self.filepath);
920
921        if is_app_entry {
922            let mut possibly_invalid_exports: FxIndexMap<Atom, (InvalidExportKind, Span)> =
923                FxIndexMap::default();
924
925            let mut collect_possibly_invalid_exports =
926                |export_name: &Atom, span: &Span| match &**export_name {
927                    "getServerSideProps" | "getStaticProps" => {
928                        possibly_invalid_exports
929                            .insert(export_name.clone(), (InvalidExportKind::General, *span));
930                    }
931                    "generateMetadata" | "metadata" => {
932                        possibly_invalid_exports
933                            .insert(export_name.clone(), (InvalidExportKind::Metadata, *span));
934                    }
935                    "runtime" => {
936                        if self.cache_components_enabled {
937                            possibly_invalid_exports.insert(
938                                export_name.clone(),
939                                (
940                                    InvalidExportKind::RouteSegmentConfig(
941                                        NextConfigProperty::CacheComponents,
942                                    ),
943                                    *span,
944                                ),
945                            );
946                        } else if self.use_cache_enabled {
947                            possibly_invalid_exports.insert(
948                                export_name.clone(),
949                                (
950                                    InvalidExportKind::RouteSegmentConfig(
951                                        NextConfigProperty::UseCache,
952                                    ),
953                                    *span,
954                                ),
955                            );
956                        }
957                    }
958                    "dynamicParams" | "dynamic" | "fetchCache" | "revalidate"
959                    | "experimental_ppr"
960                        if self.cache_components_enabled =>
961                    {
962                        possibly_invalid_exports.insert(
963                            export_name.clone(),
964                            (
965                                InvalidExportKind::RouteSegmentConfig(
966                                    NextConfigProperty::CacheComponents,
967                                ),
968                                *span,
969                            ),
970                        );
971                    }
972                    "instant" if !self.cache_components_enabled => {
973                        possibly_invalid_exports.insert(
974                            export_name.clone(),
975                            (
976                                InvalidExportKind::RequiresRouteSegmentConfig(
977                                    NextConfigProperty::CacheComponents,
978                                ),
979                                *span,
980                            ),
981                        );
982                    }
983                    _ => (),
984                };
985
986            for export in &module.body {
987                match export {
988                    ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(export)) => {
989                        for specifier in &export.specifiers {
990                            if let ExportSpecifier::Named(named) = specifier {
991                                collect_possibly_invalid_exports(&named.orig.atom(), &named.span);
992                            }
993                        }
994                    }
995                    ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export)) => match &export.decl {
996                        Decl::Fn(f) => {
997                            collect_possibly_invalid_exports(&f.ident.sym, &f.ident.span);
998                        }
999                        Decl::Var(v) => {
1000                            for decl in &v.decls {
1001                                if let Pat::Ident(i) = &decl.name {
1002                                    collect_possibly_invalid_exports(&i.sym, &i.span);
1003                                }
1004                            }
1005                        }
1006                        _ => {}
1007                    },
1008                    _ => {}
1009                }
1010            }
1011
1012            for (export_name, (kind, span)) in &possibly_invalid_exports {
1013                match kind {
1014                    InvalidExportKind::RouteSegmentConfig(property) => {
1015                        report_error(
1016                            &self.app_dir,
1017                            &self.filepath,
1018                            RSCErrorKind::NextRscErrIncompatibleRouteSegmentConfig(
1019                                *span,
1020                                export_name.to_string(),
1021                                *property,
1022                            ),
1023                        );
1024                    }
1025                    InvalidExportKind::RequiresRouteSegmentConfig(property) => {
1026                        report_error(
1027                            &self.app_dir,
1028                            &self.filepath,
1029                            RSCErrorKind::NextRscErrRequiresRouteSegmentConfig(
1030                                *span,
1031                                export_name.to_string(),
1032                                *property,
1033                            ),
1034                        );
1035                    }
1036                    InvalidExportKind::Metadata => {
1037                        // Client entry can't export `generateMetadata` or `metadata`.
1038                        if is_client_entry
1039                            && (export_name == "generateMetadata" || export_name == "metadata")
1040                        {
1041                            report_error(
1042                                &self.app_dir,
1043                                &self.filepath,
1044                                RSCErrorKind::NextRscErrClientMetadataExport((
1045                                    export_name.to_string(),
1046                                    *span,
1047                                )),
1048                            );
1049                        }
1050                        // Server entry can't export `generateMetadata` and `metadata` together,
1051                        // which is handled separately below.
1052                    }
1053                    InvalidExportKind::General => {
1054                        report_error(
1055                            &self.app_dir,
1056                            &self.filepath,
1057                            RSCErrorKind::NextRscErrInvalidApi((export_name.to_string(), *span)),
1058                        );
1059                    }
1060                }
1061            }
1062
1063            // Server entry can't export `generateMetadata` and `metadata` together.
1064            if !is_client_entry {
1065                let export1 = possibly_invalid_exports.get(&atom!("generateMetadata"));
1066                let export2 = possibly_invalid_exports.get(&atom!("metadata"));
1067
1068                if let (Some((_, span1)), Some((_, span2))) = (export1, export2) {
1069                    report_error(
1070                        &self.app_dir,
1071                        &self.filepath,
1072                        RSCErrorKind::NextRscErrConflictMetadataExport((*span1, *span2)),
1073                    );
1074                }
1075            }
1076        }
1077    }
1078
1079    /// ```js
1080    /// import dynamic from 'next/dynamic'
1081    ///
1082    /// dynamic(() => import(...)) // ✅
1083    /// dynamic(() => import(...), { ssr: true }) // ✅
1084    /// dynamic(() => import(...), { ssr: false }) // ❌
1085    /// ```
1086    fn check_for_next_ssr_false(&self, node: &CallExpr) -> Option<()> {
1087        if !self.is_callee_next_dynamic(&node.callee) {
1088            return None;
1089        }
1090
1091        let ssr_arg = node.args.get(1)?;
1092        let obj = ssr_arg.expr.as_object()?;
1093
1094        for prop in obj.props.iter().filter_map(|v| v.as_prop()?.as_key_value()) {
1095            if prop_name_eq(&prop.key, "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}