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