next_custom_transforms/transforms/
empty_gsp.rs1use regex::Regex;
2use swc_core::{
3 common::{Span, Spanned},
4 ecma::{
5 ast::*,
6 visit::{Visit, VisitMut, VisitWith, visit_mut_pass},
7 },
8 quote,
9};
10
11#[derive(Debug)]
20pub struct EmptyGenerateStaticParams {
21 page_or_layout: Regex,
22}
23
24impl EmptyGenerateStaticParams {
25 pub fn new<I, S>(page_extensions: I) -> Self
26 where
27 I: IntoIterator<Item = S>,
28 S: AsRef<str>,
29 {
30 let mut result = String::from(r"[\\/](page|layout|default)\.");
33 let mut iter = page_extensions.into_iter();
34 if let Some(first) = iter.next() {
35 result.push('(');
36 result.push_str(®ex::escape(first.as_ref()));
37 for ext in iter {
38 result.push('|');
39 result.push_str(®ex::escape(ext.as_ref()));
40 }
41 result.push(')');
42 } else {
43 result.push_str("(ts|js)x?");
44 }
45 result.push('$');
46 Self {
47 page_or_layout: Regex::new(&result).unwrap(),
48 }
49 }
50
51 pub fn get_pass(&self, filepath: String) -> impl Pass + use<> {
52 visit_mut_pass(EmptyGenerateStaticParamsPass {
53 filepath,
54 page_or_layout: self.page_or_layout.clone(),
55 })
56 }
57}
58
59struct EmptyGenerateStaticParamsPass {
60 filepath: String,
61 page_or_layout: Regex,
62}
63
64#[derive(Default)]
68struct ReturnCollector {
69 count: usize,
70 empty_array_span: Option<Span>,
71}
72
73impl Visit for ReturnCollector {
74 fn visit_return_stmt(&mut self, return_statement: &ReturnStmt) {
75 self.count += 1;
76 if let Some(argument) = &return_statement.arg
77 && let Expr::Array(array) = &**argument
78 && array.elems.is_empty()
79 {
80 self.empty_array_span = Some(array.span());
81 }
82 }
83
84 fn visit_arrow_expr(&mut self, _: &ArrowExpr) {}
85 fn visit_function(&mut self, _: &Function) {}
86}
87
88fn find_error_anchor_in_body(body: &BlockStmt) -> Option<Span> {
93 let mut returns = ReturnCollector::default();
94 body.visit_with(&mut returns);
95 if returns.count == 1 {
96 return returns.empty_array_span;
97 }
98 None
99}
100
101fn find_error_anchor_in_initializer(initializer: &Expr) -> Option<Span> {
105 match initializer {
106 Expr::Arrow(arrow) => match &*arrow.body {
107 BlockStmtOrExpr::BlockStmt(body) => find_error_anchor_in_body(body),
108 BlockStmtOrExpr::Expr(expr) => match &**expr {
109 Expr::Array(array) if array.elems.is_empty() => Some(array.span()),
110 _ => None,
111 },
112 },
113 Expr::Fn(function_expression) => function_expression
114 .function
115 .body
116 .as_ref()
117 .and_then(find_error_anchor_in_body),
118 _ => None,
119 }
120}
121
122fn find_error_anchor_in_declaration(declaration: &Decl, name: &str) -> Option<Span> {
126 match declaration {
127 Decl::Fn(function) if function.ident.sym == *name => Some(
128 function
129 .function
130 .body
131 .as_ref()
132 .and_then(find_error_anchor_in_body)
133 .unwrap_or_else(|| function.function.span()),
134 ),
135 Decl::Var(variable) => {
136 for declarator in &variable.decls {
137 if let Pat::Ident(ident) = &declarator.name
138 && ident.id.sym == *name
139 && let Some(initializer) = &declarator.init
140 {
141 return Some(
142 find_error_anchor_in_initializer(initializer)
143 .unwrap_or_else(|| initializer.span()),
144 );
145 }
146 }
147 None
148 }
149 _ => None,
150 }
151}
152
153fn find_generate_static_params_export(specifier: &ExportSpecifier) -> Option<&Ident> {
157 match specifier {
158 ExportSpecifier::Named(ExportNamedSpecifier {
159 exported: Some(ModuleExportName::Ident(exported)),
160 orig: ModuleExportName::Ident(local),
161 ..
162 }) if exported.sym == "generateStaticParams" => Some(local),
163 ExportSpecifier::Named(ExportNamedSpecifier {
164 exported: None,
165 orig: ModuleExportName::Ident(local),
166 ..
167 }) if local.sym == "generateStaticParams" => Some(local),
168 _ => None,
169 }
170}
171
172fn find_error_anchor_for_local_name(items: &[ModuleItem], name: &str) -> Option<Span> {
175 for item in items {
176 let declaration = match item {
177 ModuleItem::Stmt(Stmt::Decl(declaration)) => declaration,
178 ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export)) => &export.decl,
179 _ => continue,
180 };
181 if let Some(span) = find_error_anchor_in_declaration(declaration, name) {
182 return Some(span);
183 }
184 }
185 None
186}
187
188fn find_error_anchor(items: &[ModuleItem]) -> Option<Span> {
192 for item in items {
193 match item {
194 ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export)) => {
195 if let Some(span) =
196 find_error_anchor_in_declaration(&export.decl, "generateStaticParams")
197 {
198 return Some(span);
199 }
200 }
201 ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(named)) => {
202 for specifier in &named.specifiers {
203 if let Some(local) = find_generate_static_params_export(specifier) {
204 if named.src.is_some() {
208 return Some(specifier.span());
209 }
210 return Some(
211 find_error_anchor_for_local_name(items, &local.sym)
212 .unwrap_or_else(|| specifier.span()),
213 );
214 }
215 }
216 }
217 _ => {}
218 }
219 }
220 None
221}
222
223impl VisitMut for EmptyGenerateStaticParamsPass {
224 fn visit_mut_module_items(&mut self, items: &mut Vec<ModuleItem>) {
225 if !self.page_or_layout.is_match(&self.filepath) {
226 return;
227 }
228
229 let Some(anchor) = find_error_anchor(items) else {
230 return;
231 };
232
233 let mut error = quote!(
236 "new Error('When using Cache Components, all `generateStaticParams` \
237 functions must return at least one result. This is to ensure \
238 that we can perform build-time validation that there is no \
239 other dynamic accesses that would cause a runtime error.\\n\\n\
240 Learn more: \
241 https://nextjs.org/docs/messages/empty-generate-static-params')"
242 as Expr
243 );
244 if let Expr::New(new_expr) = &mut error {
245 new_expr.span = anchor;
246 }
247
248 let mut factory = quote!(
251 "function generateStaticParams() {
252 return $error
253 }" as Expr,
254 error: Expr = error,
255 );
256 if let Expr::Fn(f) = &mut factory {
257 f.function.span = anchor;
258 }
259
260 items.push(quote!(
261 "export const __next_create_empty_gsp_error = $factory" as ModuleItem,
262 factory: Expr = factory,
263 ));
264 }
265}