Skip to main content

next_custom_transforms/transforms/
debug_instant_stack.rs

1use regex::Regex;
2use swc_core::{
3    common::{Span, Spanned},
4    ecma::{
5        ast::*,
6        visit::{VisitMut, visit_mut_pass},
7    },
8    quote,
9};
10
11fn build_page_extensions_regex(page_extensions: &[String]) -> String {
12    if page_extensions.is_empty() {
13        "(ts|js)x?".to_string()
14    } else {
15        let escaped: Vec<String> = page_extensions
16            .iter()
17            .map(|ext| regex::escape(ext))
18            .collect();
19        format!("({})", escaped.join("|"))
20    }
21}
22
23pub fn debug_instant_stack(filepath: String, page_extensions: Vec<String>) -> impl Pass {
24    visit_mut_pass(DebugInstantStack {
25        filepath,
26        instant_export_span: None,
27        page_extensions,
28    })
29}
30
31struct DebugInstantStack {
32    filepath: String,
33    instant_export_span: Option<Span>,
34    page_extensions: Vec<String>,
35}
36
37/// Given an export specifier, returns `Some((exported_name, local_name))` if
38/// the exported name is `unstable_instant`.
39fn get_instant_specifier_names(specifier: &ExportSpecifier) -> Option<(&Ident, &Ident)> {
40    match specifier {
41        // `export { orig as unstable_instant }`
42        ExportSpecifier::Named(ExportNamedSpecifier {
43            exported: Some(ModuleExportName::Ident(exported)),
44            orig: ModuleExportName::Ident(orig),
45            ..
46        }) if exported.sym == "unstable_instant" => Some((exported, orig)),
47        // `export { unstable_instant }`
48        ExportSpecifier::Named(ExportNamedSpecifier {
49            exported: None,
50            orig: ModuleExportName::Ident(orig),
51            ..
52        }) if orig.sym == "unstable_instant" => Some((orig, orig)),
53        _ => None,
54    }
55}
56
57/// Find the initializer span of a variable declaration with the given name.
58fn find_var_init_span(items: &[ModuleItem], local_name: &str) -> Option<Span> {
59    for item in items {
60        let decl = match item {
61            ModuleItem::Stmt(Stmt::Decl(Decl::Var(var_decl))) => var_decl,
62            ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export_decl)) => {
63                if let Decl::Var(var_decl) = &export_decl.decl {
64                    var_decl
65                } else {
66                    continue;
67                }
68            }
69            _ => continue,
70        };
71        for d in &decl.decls {
72            if let Pat::Ident(ident) = &d.name
73                && ident.id.sym == local_name
74                && let Some(init) = &d.init
75            {
76                return Some(init.span());
77            }
78        }
79    }
80    None
81}
82
83impl VisitMut for DebugInstantStack {
84    fn visit_mut_module_items(&mut self, items: &mut Vec<ModuleItem>) {
85        let ext_pattern = build_page_extensions_regex(&self.page_extensions);
86        let page_or_layout_re =
87            Regex::new(&format!(r"[\\/](page|layout|default)\.{ext_pattern}$")).unwrap();
88        if !page_or_layout_re.is_match(&self.filepath) {
89            return;
90        }
91
92        for item in items.iter() {
93            match item {
94                // `export const unstable_instant = ...`
95                ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export_decl)) => {
96                    if let Decl::Var(var_decl) = &export_decl.decl {
97                        for decl in &var_decl.decls {
98                            if let Pat::Ident(ident) = &decl.name
99                                && ident.id.sym == "unstable_instant"
100                                && let Some(init) = &decl.init
101                            {
102                                self.instant_export_span = Some(init.span());
103                            }
104                        }
105                    }
106                }
107                // `export { unstable_instant }` or `export { x as unstable_instant }`
108                // with or without `from '...'`
109                ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(named)) => {
110                    for specifier in &named.specifiers {
111                        if let Some((_exported, orig)) = get_instant_specifier_names(specifier) {
112                            if named.src.is_some() {
113                                // Re-export: `export { unstable_instant } from './config'`
114                                // Point at the export specifier itself
115                                self.instant_export_span = Some(specifier.span());
116                            } else {
117                                // Local named export: try to find the variable's initializer
118                                let local_name = &orig.sym;
119                                if let Some(init_span) = find_var_init_span(items, local_name) {
120                                    self.instant_export_span = Some(init_span);
121                                } else {
122                                    // Fallback to the export specifier span
123                                    self.instant_export_span = Some(specifier.span());
124                                }
125                            }
126                        }
127                    }
128                }
129                _ => {}
130            }
131        }
132
133        if let Some(source_span) = self.instant_export_span {
134            // TODO: Change React to deserialize errors with a zero-length message
135            // instead of using a fallback message ("no message was provided").
136            // We're working around this by using a message that is empty
137            // after trimming but isn't to JavaScript before trimming (' '.length === 1).
138            let mut new_error = quote!("new Error(' ')" as Expr);
139            if let Expr::New(new_expr) = &mut new_error {
140                new_expr.span = source_span;
141            }
142
143            let mut cons = quote!(
144                "function unstable_instant() {
145                    const error = $new_error
146                    error.name = 'Instant Validation'
147                    return error
148                }" as Expr,
149                new_error: Expr = new_error,
150            );
151
152            // Patch source_span onto the Function
153            // for sourcemap mapping back to the unstable_instant config value
154            if let Expr::Fn(f) = &mut cons {
155                f.function.span = source_span;
156            }
157
158            let export = quote!(
159                "export const __debugCreateInstantConfigStack =
160                    process.env.NODE_ENV !== 'production' ? $cons : null"
161                    as ModuleItem,
162                cons: Expr = cons,
163            );
164
165            items.push(export);
166        }
167    }
168}