next_custom_transforms/transforms/
page_config.rs

1use chrono::Utc;
2use swc_core::{
3    common::{errors::HANDLER, Span, DUMMY_SP},
4    ecma::{
5        ast::*,
6        visit::{fold_pass, Fold, FoldWith},
7    },
8};
9
10pub fn page_config(is_development: bool, is_page_file: bool) -> impl Pass {
11    fold_pass(PageConfig {
12        is_development,
13        is_page_file,
14        ..Default::default()
15    })
16}
17
18pub fn page_config_test() -> impl Pass {
19    fold_pass(PageConfig {
20        in_test: true,
21        is_page_file: true,
22        ..Default::default()
23    })
24}
25
26#[derive(Debug, Default)]
27struct PageConfig {
28    drop_bundle: bool,
29    in_test: bool,
30    is_development: bool,
31    is_page_file: bool,
32}
33
34const STRING_LITERAL_DROP_BUNDLE: &str = "__NEXT_DROP_CLIENT_FILE__";
35const CONFIG_KEY: &str = "config";
36
37/// TODO: Implement this as a [Pass] instead of a full visitor ([Fold])
38impl Fold for PageConfig {
39    fn fold_module_items(&mut self, items: Vec<ModuleItem>) -> Vec<ModuleItem> {
40        let mut new_items = vec![];
41        for item in items {
42            new_items.push(item.fold_with(self));
43            if !self.is_development && self.drop_bundle {
44                let timestamp = match self.in_test {
45                    true => String::from("mock_timestamp"),
46                    false => Utc::now().timestamp().to_string(),
47                };
48                return vec![ModuleItem::Stmt(Stmt::Decl(Decl::Var(Box::new(VarDecl {
49                    decls: vec![VarDeclarator {
50                        name: Pat::Ident(BindingIdent {
51                            id: Ident {
52                                sym: STRING_LITERAL_DROP_BUNDLE.into(),
53                                ..Default::default()
54                            },
55                            type_ann: None,
56                        }),
57                        init: Some(Box::new(Expr::Lit(Lit::Str(Str {
58                            value: format!("{STRING_LITERAL_DROP_BUNDLE} {timestamp}").into(),
59                            span: DUMMY_SP,
60                            raw: None,
61                        })))),
62                        span: DUMMY_SP,
63                        definite: false,
64                    }],
65                    span: DUMMY_SP,
66                    kind: VarDeclKind::Const,
67                    ..Default::default()
68                }))))];
69            }
70        }
71
72        new_items
73    }
74
75    fn fold_export_decl(&mut self, export: ExportDecl) -> ExportDecl {
76        if let Decl::Var(var_decl) = &export.decl {
77            for decl in &var_decl.decls {
78                let mut is_config = false;
79                if let Pat::Ident(ident) = &decl.name {
80                    if ident.id.sym == CONFIG_KEY {
81                        is_config = true;
82                    }
83                }
84
85                if is_config {
86                    if let Some(expr) = &decl.init {
87                        if let Expr::Object(obj) = &**expr {
88                            for prop in &obj.props {
89                                if let PropOrSpread::Prop(prop) = prop {
90                                    if let Prop::KeyValue(kv) = &**prop {
91                                        match &kv.key {
92                                            PropName::Ident(ident) => {
93                                                if &ident.sym == "amp" {
94                                                    if let Expr::Lit(Lit::Bool(Bool {
95                                                        value,
96                                                        ..
97                                                    })) = &*kv.value
98                                                    {
99                                                        if *value && self.is_page_file {
100                                                            self.drop_bundle = true;
101                                                        }
102                                                    } else if let Expr::Lit(Lit::Str(_)) =
103                                                        &*kv.value
104                                                    {
105                                                        // Do not replace
106                                                        // bundle
107                                                    } else {
108                                                        self.handle_error(
109                                                            "Invalid value found.",
110                                                            export.span,
111                                                        );
112                                                    }
113                                                }
114                                            }
115                                            _ => {
116                                                self.handle_error(
117                                                    "Invalid property found.",
118                                                    export.span,
119                                                );
120                                            }
121                                        }
122                                    } else {
123                                        self.handle_error(
124                                            "Invalid property or value.",
125                                            export.span,
126                                        );
127                                    }
128                                } else {
129                                    self.handle_error(
130                                        "Property spread is not allowed.",
131                                        export.span,
132                                    );
133                                }
134                            }
135                        } else {
136                            self.handle_error("Expected config to be an object.", export.span);
137                        }
138                    } else {
139                        self.handle_error("Expected config to be an object.", export.span);
140                    }
141                }
142            }
143        }
144        export
145    }
146
147    fn fold_export_named_specifier(
148        &mut self,
149        specifier: ExportNamedSpecifier,
150    ) -> ExportNamedSpecifier {
151        match &specifier.exported {
152            Some(ident) => {
153                if let ModuleExportName::Ident(ident) = ident {
154                    if ident.sym == CONFIG_KEY {
155                        self.handle_error("Config cannot be re-exported.", specifier.span)
156                    }
157                }
158            }
159            None => {
160                if let ModuleExportName::Ident(ident) = &specifier.orig {
161                    if ident.sym == CONFIG_KEY {
162                        self.handle_error("Config cannot be re-exported.", specifier.span)
163                    }
164                }
165            }
166        }
167        specifier
168    }
169}
170
171impl PageConfig {
172    fn handle_error(&mut self, details: &str, span: Span) {
173        if self.is_page_file {
174            let message = format!("Invalid page config export found. {details} \
175      See: https://nextjs.org/docs/messages/invalid-page-config");
176            HANDLER.with(|handler| handler.struct_span_err(span, &message).emit());
177        }
178    }
179}