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