next_custom_transforms/transforms/
react_server_components.rs

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