Skip to main content

next_custom_transforms/transforms/
react_server_components.rs

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