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