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                        "useEffectEvent",
637                        "useImperativeHandle",
638                        "useInsertionEffect",
639                        "useLayoutEffect",
640                        "useReducer",
641                        "useRef",
642                        "useState",
643                        "useSyncExternalStore",
644                        "useTransition",
645                        "useOptimistic",
646                        "useActionState",
647                        "experimental_useOptimistic",
648                    ],
649                ),
650                (
651                    atom!("react-dom").into(),
652                    vec![
653                        "flushSync",
654                        "unstable_batchedUpdates",
655                        "useFormStatus",
656                        "useFormState",
657                    ],
658                ),
659                (
660                    atom!("next/navigation").into(),
661                    vec![
662                        "useSearchParams",
663                        "usePathname",
664                        "useSelectedLayoutSegment",
665                        "useSelectedLayoutSegments",
666                        "useParams",
667                        "useRouter",
668                        "useServerInsertedHTML",
669                        "ServerInsertedHTMLContext",
670                        "unstable_isUnrecognizedActionError",
671                    ],
672                ),
673                (atom!("next/link").into(), vec!["useLinkStatus"]),
674            ]),
675            deprecated_apis_mapping: FxHashMap::from_iter([(
676                atom!("next/server").into(),
677                vec!["ImageResponse"],
678            )]),
679
680            invalid_server_imports: vec![
681                atom!("client-only").into(),
682                atom!("react-dom/client").into(),
683                atom!("react-dom/server").into(),
684                atom!("next/router").into(),
685            ],
686
687            invalid_client_imports: vec![
688                atom!("server-only").into(),
689                atom!("next/headers").into(),
690                atom!("next/root-params").into(),
691            ],
692
693            invalid_client_lib_apis_mapping: FxHashMap::from_iter([
694                (atom!("next/server").into(), vec!["after"]),
695                (
696                    atom!("next/cache").into(),
697                    vec![
698                        "revalidatePath",
699                        "revalidateTag",
700                        // "unstable_cache", // useless in client, but doesn't technically error
701                        "cacheLife",
702                        "unstable_cacheLife",
703                        "cacheTag",
704                        "unstable_cacheTag",
705                        // "unstable_noStore" // no-op in client, but allowed for legacy reasons
706                    ],
707                ),
708            ]),
709            react_taint_apis: vec![
710                "experimental_taintObjectReference",
711                "experimental_taintUniqueValue",
712            ],
713            imports: ImportMap::default(),
714        }
715    }
716
717    fn is_from_node_modules(&self, filepath: &str) -> bool {
718        static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"node_modules[\\/]").unwrap());
719        RE.is_match(filepath)
720    }
721
722    fn is_callee_next_dynamic(&self, callee: &Callee) -> bool {
723        match callee {
724            Callee::Expr(expr) => self.imports.is_import(expr, "next/dynamic", "default"),
725            _ => false,
726        }
727    }
728
729    // Asserts the server lib apis
730    // e.g.
731    // assert_invalid_server_lib_apis("react", import)
732    // assert_invalid_server_lib_apis("react-dom", import)
733    fn assert_invalid_server_lib_apis(&self, import_source: &Wtf8Atom, import: &ModuleImports) {
734        let deprecated_apis = self.deprecated_apis_mapping.get(import_source);
735        if let Some(deprecated_apis) = deprecated_apis {
736            for specifier in &import.specifiers {
737                if deprecated_apis.contains(&specifier.0.as_str()) {
738                    report_error(
739                        &self.app_dir,
740                        &self.filepath,
741                        RSCErrorKind::NextRscErrDeprecatedApi((
742                            import_source.to_string_lossy().into_owned(),
743                            specifier.0.to_string(),
744                            specifier.1,
745                        )),
746                    );
747                }
748            }
749        }
750
751        let invalid_apis = self.invalid_server_lib_apis_mapping.get(import_source);
752        if let Some(invalid_apis) = invalid_apis {
753            for specifier in &import.specifiers {
754                if invalid_apis.contains(&specifier.0.as_str()) {
755                    report_error(
756                        &self.app_dir,
757                        &self.filepath,
758                        RSCErrorKind::NextRscErrReactApi((specifier.0.to_string(), specifier.1)),
759                    );
760                }
761            }
762        }
763    }
764
765    /// Check for React taint API imports when taint is not enabled
766    fn assert_react_taint_apis(&self, imports: &[ModuleImports]) {
767        // Skip check if taint is enabled or if file is from node_modules
768        if self.taint_enabled || self.is_from_node_modules(&self.filepath) {
769            return;
770        }
771
772        for import in imports {
773            let source = &import.source.0;
774            // Only check imports from 'react'
775            if source.as_str() != Some("react") {
776                continue;
777            }
778
779            for specifier in &import.specifiers {
780                if self.react_taint_apis.contains(&specifier.0.as_str()) {
781                    report_error(
782                        &self.app_dir,
783                        &self.filepath,
784                        RSCErrorKind::NextRscErrTaintWithoutConfig((
785                            specifier.0.to_string(),
786                            specifier.1,
787                        )),
788                    );
789                }
790            }
791        }
792    }
793
794    fn assert_server_graph(&self, imports: &[ModuleImports], module: &Module) {
795        // If the
796        if self.is_from_node_modules(&self.filepath) {
797            return;
798        }
799        for import in imports {
800            let source = &import.source.0;
801            if self.invalid_server_imports.contains(source) {
802                report_error(
803                    &self.app_dir,
804                    &self.filepath,
805                    RSCErrorKind::NextRscErrServerImport((
806                        source.to_string_lossy().into_owned(),
807                        import.source.1,
808                    )),
809                );
810            }
811
812            self.assert_invalid_server_lib_apis(source, import);
813        }
814
815        self.assert_invalid_api(module, false);
816        self.assert_server_filename(module);
817    }
818
819    fn assert_server_filename(&self, module: &Module) {
820        if self.is_from_node_modules(&self.filepath) {
821            return;
822        }
823        static RE: Lazy<Regex> =
824            Lazy::new(|| Regex::new(r"[\\/]((global-)?error)\.(ts|js)x?$").unwrap());
825
826        let is_error_file = RE.is_match(&self.filepath);
827
828        if is_error_file {
829            if let Some(app_dir) = &self.app_dir {
830                if let Some(app_dir) = app_dir.to_str() {
831                    if self.filepath.starts_with(app_dir) {
832                        let span = if let Some(first_item) = module.body.first() {
833                            first_item.span()
834                        } else {
835                            module.span
836                        };
837
838                        report_error(
839                            &self.app_dir,
840                            &self.filepath,
841                            RSCErrorKind::NextRscErrErrorFileServerComponent(span),
842                        );
843                    }
844                }
845            }
846        }
847    }
848
849    fn assert_client_graph(&self, imports: &[ModuleImports]) {
850        if self.is_from_node_modules(&self.filepath) {
851            return;
852        }
853        for import in imports {
854            let source = &import.source.0;
855
856            if self.invalid_client_imports.contains(source) {
857                report_error(
858                    &self.app_dir,
859                    &self.filepath,
860                    RSCErrorKind::NextRscErrClientImport((
861                        source.to_string_lossy().into_owned(),
862                        import.source.1,
863                    )),
864                );
865            }
866
867            let invalid_apis = self.invalid_client_lib_apis_mapping.get(source);
868            if let Some(invalid_apis) = invalid_apis {
869                for specifier in &import.specifiers {
870                    if invalid_apis.contains(&specifier.0.as_str()) {
871                        report_error(
872                            &self.app_dir,
873                            &self.filepath,
874                            RSCErrorKind::NextRscErrClientImport((
875                                specifier.0.to_string(),
876                                specifier.1,
877                            )),
878                        );
879                    }
880                }
881            }
882        }
883    }
884
885    fn assert_invalid_api(&self, module: &Module, is_client_entry: bool) {
886        if self.is_from_node_modules(&self.filepath) {
887            return;
888        }
889        static RE: Lazy<Regex> =
890            Lazy::new(|| Regex::new(r"[\\/](page|layout|route)\.(ts|js)x?$").unwrap());
891        let is_app_entry = RE.is_match(&self.filepath);
892
893        if is_app_entry {
894            let mut possibly_invalid_exports: FxIndexMap<Atom, (InvalidExportKind, Span)> =
895                FxIndexMap::default();
896
897            let mut collect_possibly_invalid_exports =
898                |export_name: &Atom, span: &Span| match &**export_name {
899                    "getServerSideProps" | "getStaticProps" => {
900                        possibly_invalid_exports
901                            .insert(export_name.clone(), (InvalidExportKind::General, *span));
902                    }
903                    "generateMetadata" | "metadata" => {
904                        possibly_invalid_exports
905                            .insert(export_name.clone(), (InvalidExportKind::Metadata, *span));
906                    }
907                    "runtime" => {
908                        if self.cache_components_enabled {
909                            possibly_invalid_exports.insert(
910                                export_name.clone(),
911                                (
912                                    InvalidExportKind::RouteSegmentConfig(
913                                        NextConfigProperty::CacheComponents,
914                                    ),
915                                    *span,
916                                ),
917                            );
918                        } else if self.use_cache_enabled {
919                            possibly_invalid_exports.insert(
920                                export_name.clone(),
921                                (
922                                    InvalidExportKind::RouteSegmentConfig(
923                                        NextConfigProperty::UseCache,
924                                    ),
925                                    *span,
926                                ),
927                            );
928                        }
929                    }
930                    "dynamicParams" | "dynamic" | "fetchCache" | "revalidate"
931                    | "experimental_ppr" => {
932                        if self.cache_components_enabled {
933                            possibly_invalid_exports.insert(
934                                export_name.clone(),
935                                (
936                                    InvalidExportKind::RouteSegmentConfig(
937                                        NextConfigProperty::CacheComponents,
938                                    ),
939                                    *span,
940                                ),
941                            );
942                        }
943                    }
944                    _ => (),
945                };
946
947            for export in &module.body {
948                match export {
949                    ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(export)) => {
950                        for specifier in &export.specifiers {
951                            if let ExportSpecifier::Named(named) = specifier {
952                                collect_possibly_invalid_exports(&named.orig.atom(), &named.span);
953                            }
954                        }
955                    }
956                    ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export)) => match &export.decl {
957                        Decl::Fn(f) => {
958                            collect_possibly_invalid_exports(&f.ident.sym, &f.ident.span);
959                        }
960                        Decl::Var(v) => {
961                            for decl in &v.decls {
962                                if let Pat::Ident(i) = &decl.name {
963                                    collect_possibly_invalid_exports(&i.sym, &i.span);
964                                }
965                            }
966                        }
967                        _ => {}
968                    },
969                    _ => {}
970                }
971            }
972
973            for (export_name, (kind, span)) in &possibly_invalid_exports {
974                match kind {
975                    InvalidExportKind::RouteSegmentConfig(property) => {
976                        report_error(
977                            &self.app_dir,
978                            &self.filepath,
979                            RSCErrorKind::NextRscErrIncompatibleRouteSegmentConfig(
980                                *span,
981                                export_name.to_string(),
982                                *property,
983                            ),
984                        );
985                    }
986                    InvalidExportKind::Metadata => {
987                        // Client entry can't export `generateMetadata` or `metadata`.
988                        if is_client_entry
989                            && (export_name == "generateMetadata" || export_name == "metadata")
990                        {
991                            report_error(
992                                &self.app_dir,
993                                &self.filepath,
994                                RSCErrorKind::NextRscErrClientMetadataExport((
995                                    export_name.to_string(),
996                                    *span,
997                                )),
998                            );
999                        }
1000                        // Server entry can't export `generateMetadata` and `metadata` together,
1001                        // which is handled separately below.
1002                    }
1003                    InvalidExportKind::General => {
1004                        report_error(
1005                            &self.app_dir,
1006                            &self.filepath,
1007                            RSCErrorKind::NextRscErrInvalidApi((export_name.to_string(), *span)),
1008                        );
1009                    }
1010                }
1011            }
1012
1013            // Server entry can't export `generateMetadata` and `metadata` together.
1014            if !is_client_entry {
1015                let export1 = possibly_invalid_exports.get(&atom!("generateMetadata"));
1016                let export2 = possibly_invalid_exports.get(&atom!("metadata"));
1017
1018                if let (Some((_, span1)), Some((_, span2))) = (export1, export2) {
1019                    report_error(
1020                        &self.app_dir,
1021                        &self.filepath,
1022                        RSCErrorKind::NextRscErrConflictMetadataExport((*span1, *span2)),
1023                    );
1024                }
1025            }
1026        }
1027    }
1028
1029    /// ```js
1030    /// import dynamic from 'next/dynamic'
1031    ///
1032    /// dynamic(() => import(...)) // ✅
1033    /// dynamic(() => import(...), { ssr: true }) // ✅
1034    /// dynamic(() => import(...), { ssr: false }) // ❌
1035    /// ```
1036    fn check_for_next_ssr_false(&self, node: &CallExpr) -> Option<()> {
1037        if !self.is_callee_next_dynamic(&node.callee) {
1038            return None;
1039        }
1040
1041        let ssr_arg = node.args.get(1)?;
1042        let obj = ssr_arg.expr.as_object()?;
1043
1044        for prop in obj.props.iter().filter_map(|v| v.as_prop()?.as_key_value()) {
1045            let is_ssr = match &prop.key {
1046                PropName::Ident(IdentName { sym, .. }) => sym == "ssr",
1047                PropName::Str(s) => s.value == "ssr",
1048                _ => false,
1049            };
1050
1051            if is_ssr {
1052                let value = prop.value.as_lit()?;
1053                if let Lit::Bool(Bool { value: false, .. }) = value {
1054                    report_error(
1055                        &self.app_dir,
1056                        &self.filepath,
1057                        RSCErrorKind::NextSsrDynamicFalseNotAllowed(node.span),
1058                    );
1059                }
1060            }
1061        }
1062
1063        None
1064    }
1065}
1066
1067impl Visit for ReactServerComponentValidator {
1068    noop_visit_type!();
1069
1070    // coerce parsed script to run validation for the context, which is still
1071    // required even if file is empty
1072    fn visit_script(&mut self, script: &swc_core::ecma::ast::Script) {
1073        if script.body.is_empty() {
1074            self.visit_module(&Module::dummy());
1075        }
1076    }
1077
1078    fn visit_call_expr(&mut self, node: &CallExpr) {
1079        node.visit_children_with(self);
1080
1081        if self.is_react_server_layer {
1082            self.check_for_next_ssr_false(node);
1083        }
1084    }
1085
1086    fn visit_module(&mut self, module: &Module) {
1087        self.imports = ImportMap::analyze(module);
1088
1089        let (directive, imports, export_names) =
1090            collect_module_info(&self.app_dir, &self.filepath, module);
1091        let imports = Rc::new(imports);
1092
1093        self.module_directive = directive;
1094        self.export_names = export_names;
1095
1096        // Check for taint API usage without config (runs for all files)
1097        self.assert_react_taint_apis(&imports);
1098
1099        if self.is_react_server_layer {
1100            if directive == Some(ModuleDirective::UseClient) {
1101                return;
1102            } else {
1103                // Only assert server graph if file's bundle target is "server", e.g.
1104                // * server components pages
1105                // * pages bundles on SSR layer
1106                // * middleware
1107                // * app/pages api routes
1108                self.assert_server_graph(&imports, module);
1109            }
1110        } else {
1111            // Only assert client graph if the file is not an action or cache file,
1112            // and bundle target is "client" e.g.
1113            // * client components pages
1114            // * pages bundles on browser layer
1115            if directive != Some(ModuleDirective::UseServer)
1116                && directive != Some(ModuleDirective::UseCache)
1117            {
1118                self.assert_client_graph(&imports);
1119                self.assert_invalid_api(module, true);
1120            }
1121        }
1122
1123        module.visit_children_with(self);
1124    }
1125}
1126
1127/// Returns a visitor to assert react server components without any transform.
1128/// This is for the Turbopack which have its own transform phase for the server
1129/// components proxy.
1130///
1131/// This also returns a visitor instead of fold and performs better than running
1132/// whole transform as a folder.
1133pub fn server_components_assert(
1134    filename: FileName,
1135    config: Config,
1136    app_dir: Option<PathBuf>,
1137) -> impl Visit {
1138    let is_react_server_layer: bool = match &config {
1139        Config::WithOptions(x) => x.is_react_server_layer,
1140        _ => false,
1141    };
1142    let cache_components_enabled: bool = match &config {
1143        Config::WithOptions(x) => x.cache_components_enabled,
1144        _ => false,
1145    };
1146    let use_cache_enabled: bool = match &config {
1147        Config::WithOptions(x) => x.use_cache_enabled,
1148        _ => false,
1149    };
1150    let taint_enabled: bool = match &config {
1151        Config::WithOptions(x) => x.taint_enabled,
1152        _ => false,
1153    };
1154    let filename = match filename {
1155        FileName::Custom(path) => format!("<{path}>"),
1156        _ => filename.to_string(),
1157    };
1158    ReactServerComponentValidator::new(
1159        is_react_server_layer,
1160        cache_components_enabled,
1161        use_cache_enabled,
1162        taint_enabled,
1163        filename,
1164        app_dir,
1165    )
1166}
1167
1168/// Runs react server component transform for the module proxy, as well as
1169/// running assertion.
1170pub fn server_components<C: Comments>(
1171    filename: Arc<FileName>,
1172    config: Config,
1173    comments: C,
1174    app_dir: Option<PathBuf>,
1175) -> impl Pass + VisitMut {
1176    let is_react_server_layer: bool = match &config {
1177        Config::WithOptions(x) => x.is_react_server_layer,
1178        _ => false,
1179    };
1180    let cache_components_enabled: bool = match &config {
1181        Config::WithOptions(x) => x.cache_components_enabled,
1182        _ => false,
1183    };
1184    let use_cache_enabled: bool = match &config {
1185        Config::WithOptions(x) => x.use_cache_enabled,
1186        _ => false,
1187    };
1188    let taint_enabled: bool = match &config {
1189        Config::WithOptions(x) => x.taint_enabled,
1190        _ => false,
1191    };
1192    visit_mut_pass(ReactServerComponents {
1193        is_react_server_layer,
1194        cache_components_enabled,
1195        use_cache_enabled,
1196        taint_enabled,
1197        comments,
1198        filepath: match &*filename {
1199            FileName::Custom(path) => format!("<{path}>"),
1200            _ => filename.to_string(),
1201        },
1202        app_dir,
1203    })
1204}