| | use std::fs; |
| |
|
| | use rustc_hash::FxHashMap; |
| | use swc_core::{ |
| | ecma::{ast::*, transforms::testing::test_inline, visit::*}, |
| | plugin::{plugin_transform, proxies::TransformPluginProgramMetadata}, |
| | }; |
| |
|
| | pub struct TransformVisitor { |
| | errors: FxHashMap<String, String>, |
| | } |
| |
|
| | #[derive(serde::Serialize)] |
| | #[serde(rename_all = "camelCase")] |
| | struct NewError { |
| | error_message: String, |
| | } |
| |
|
| | fn is_error_class_name(name: &str) -> bool { |
| | |
| | name == "AggregateError" |
| | |
| | || name == "Error" |
| | || name == "EvalError" |
| | || name == "RangeError" |
| | || name == "ReferenceError" |
| | || name == "SyntaxError" |
| | || name == "TypeError" |
| | || name == "URIError" |
| | |
| | || name == "ApiError" |
| | || name == "BailoutToCSRError" |
| | || name == "BubbledError" |
| | || name == "CanaryOnlyError" |
| | || name == "Cancel" |
| | || name == "CompileError" |
| | || name == "CssSyntaxError" |
| | || name == "DecodeError" |
| | || name == "DynamicServerError" |
| | || name == "ExportError" |
| | || name == "FatalError" |
| | || name == "ImageError" |
| | || name == "InvariantError" |
| | || name == "ModuleBuildError" |
| | || name == "NestedMiddlewareError" |
| | || name == "NoFallbackError" |
| | || name == "NoSuchDeclarationError" |
| | || name == "PageSignatureError" |
| | || name == "PostCSSSyntaxError" |
| | || name == "ReadonlyHeadersError" |
| | || name == "ReadonlyRequestCookiesError" |
| | || name == "ReadonlyURLSearchParamsError" |
| | || name == "ResponseAborted" |
| | || name == "SerializableError" |
| | || name == "StaticGenBailoutError" |
| | || name == "TimeoutError" |
| | || name == "UnrecognizedActionError" |
| | || name == "Warning" |
| | } |
| |
|
| | |
| | fn stringify_new_error_arg(expr: &Expr) -> String { |
| | match expr { |
| | Expr::Lit(lit) => match lit { |
| | Lit::Str(str_lit) => str_lit.value.to_string(), |
| | _ => "%s".to_string(), |
| | }, |
| |
|
| | Expr::Tpl(tpl) => { |
| | let mut result = String::new(); |
| | let mut expr_iter = tpl.exprs.iter(); |
| |
|
| | for (_i, quasi) in tpl.quasis.iter().enumerate() { |
| | result.push_str(&quasi.raw); |
| | if let Some(expr) = expr_iter.next() { |
| | result.push_str(&stringify_new_error_arg(expr)); |
| | } |
| | } |
| | result |
| | } |
| |
|
| | Expr::Bin(bin_expr) => { |
| | |
| | format!( |
| | "{}{}", |
| | stringify_new_error_arg(&bin_expr.left), |
| | stringify_new_error_arg(&bin_expr.right) |
| | ) |
| | } |
| |
|
| | _ => "%s".to_string(), |
| | } |
| | } |
| |
|
| | impl TransformVisitor {} |
| |
|
| | impl VisitMut for TransformVisitor { |
| | fn visit_mut_expr(&mut self, expr: &mut Expr) { |
| | let mut error_message: Option<String> = None; |
| |
|
| | |
| | |
| | let mut new_error_expr: Option<NewExpr> = None; |
| |
|
| | |
| | |
| | match expr { |
| | Expr::New(new_expr) => match &*new_expr.callee { |
| | Expr::Ident(ident) if is_error_class_name(ident.sym.as_str()) => { |
| | if let Some(args) = &new_expr.args { |
| | if let Some(first_arg) = args.first() { |
| | new_error_expr = Some(new_expr.clone()); |
| | error_message = Some(stringify_new_error_arg(&first_arg.expr)); |
| | } |
| | } |
| | } |
| | _ => {} |
| | }, |
| | Expr::Call(call_expr) => match &call_expr.callee { |
| | Callee::Expr(expr) => match &**expr { |
| | Expr::Ident(ident) if is_error_class_name(ident.sym.as_str()) => { |
| | if let Some(first_arg) = call_expr.args.first() { |
| | error_message = Some(stringify_new_error_arg(&first_arg.expr)); |
| |
|
| | |
| | |
| | new_error_expr = Some(NewExpr { |
| | span: call_expr.span, |
| | callee: Box::new(Expr::Ident(ident.clone())), |
| | args: Some(call_expr.args.clone()), |
| | type_args: None, |
| | ctxt: call_expr.ctxt, |
| | }); |
| | } |
| | } |
| | _ => {} |
| | }, |
| | _ => {} |
| | }, |
| | _ => {} |
| | } |
| |
|
| | if new_error_expr.is_none() || error_message.is_none() { |
| | assert!( |
| | new_error_expr.is_none() && error_message.is_none(), |
| | "Expected both new_error_expr and error_message to be None, but new_error_expr is \ |
| | {:?} and error_message is {:?}", |
| | new_error_expr, |
| | error_message |
| | ); |
| | expr.visit_mut_children_with(self); |
| | return; |
| | } |
| |
|
| | let new_error_expr: NewExpr = new_error_expr.unwrap(); |
| |
|
| | |
| | |
| | let error_message = error_message.unwrap().replace("\r\n", "\n"); |
| |
|
| | let code = self.errors.iter().find_map(|(key, value)| { |
| | |
| | if *value == error_message { |
| | Some(key) |
| | } else { |
| | None |
| | } |
| | }); |
| |
|
| | if code.is_none() { |
| | let new_error = serde_json::to_string(&NewError { error_message }).unwrap(); |
| |
|
| | let hash_hex = format!("{:x}", md5::compute(new_error.as_bytes())); |
| | let file_path = format!("cwd/.errors/{}.json", &hash_hex[0..8]); |
| |
|
| | let _ = fs::create_dir_all("cwd/.errors"); |
| | let _ = fs::write(&file_path, new_error); |
| | } else { |
| | let code = format!("E{}", code.unwrap()); |
| |
|
| | |
| | |
| | *expr = Expr::Call(CallExpr { |
| | span: new_error_expr.span, |
| | callee: Callee::Expr(Box::new(Expr::Member(MemberExpr { |
| | span: new_error_expr.span, |
| | obj: Box::new(Expr::Ident(Ident::new( |
| | rcstr!("Object"), |
| | new_error_expr.span, |
| | Default::default(), |
| | ))), |
| | prop: MemberProp::Ident(rcstr!("defineProperty")), |
| | }))), |
| | args: vec![ |
| | ExprOrSpread { |
| | spread: None, |
| | expr: Box::new(Expr::New(new_error_expr.clone())), |
| | }, |
| | ExprOrSpread { |
| | spread: None, |
| | expr: Box::new(Expr::Lit(Lit::Str(Str { |
| | span: new_error_expr.span, |
| | value: rcstr!("__NEXT_ERROR_CODE"), |
| | raw: None, |
| | }))), |
| | }, |
| | ExprOrSpread { |
| | spread: None, |
| | expr: Box::new(Expr::Object(ObjectLit { |
| | span: new_error_expr.span, |
| | props: vec![ |
| | PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { |
| | key: PropName::Ident(rcstr!("value")), |
| | value: Box::new(Expr::Lit(Lit::Str(Str { |
| | span: new_error_expr.span, |
| | value: code.into(), |
| | raw: None, |
| | }))), |
| | }))), |
| | PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { |
| | key: PropName::Ident(rcstr!("enumerable")), |
| | value: Box::new(Expr::Lit(Lit::Bool(Bool { |
| | span: new_error_expr.span, |
| | value: false, |
| | }))), |
| | }))), |
| | PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { |
| | key: PropName::Ident(rcstr!("configurable")), |
| | value: Box::new(Expr::Lit(Lit::Bool(Bool { |
| | span: new_error_expr.span, |
| | value: true, |
| | }))), |
| | }))), |
| | ], |
| | })), |
| | }, |
| | ], |
| | type_args: None, |
| | ctxt: new_error_expr.ctxt, |
| | }); |
| | } |
| | } |
| | } |
| |
|
| | #[plugin_transform] |
| | pub fn process_transform( |
| | mut program: Program, |
| | _metadata: TransformPluginProgramMetadata, |
| | ) -> Program { |
| | let errors_json = fs::read_to_string("/cwd/errors.json") |
| | .unwrap_or_else(|e| panic!("failed to read errors.json: {}", e)); |
| | let errors: FxHashMap<String, String> = serde_json::from_str(&errors_json) |
| | .unwrap_or_else(|e| panic!("failed to parse errors.json: {}", e)); |
| |
|
| | let mut visitor = TransformVisitor { errors }; |
| |
|
| | visitor.visit_mut_program(&mut program); |
| | program |
| | } |
| |
|
| | test_inline!( |
| | Default::default(), |
| | |_| visit_mut_pass(TransformVisitor { |
| | errors: FxHashMap::from_iter([ |
| | ("1".to_string(), "Failed to fetch user %s: %s".to_string()), |
| | ("2".to_string(), "Request failed: %s".to_string()), |
| | ("3".to_string(), "Generic error".to_string()), |
| | ("4".to_string(), "Empty error".to_string()), |
| | ( |
| | "5".to_string(), |
| | "Pattern should define hostname but found\n%s".to_string() |
| | ), |
| | ]), |
| | }), |
| | realistic_api_handler, |
| | |
| | r#" |
| | async function fetchUserData(userId) { |
| | try { |
| | const response = await fetch(`/api/users/${userId}`); |
| | if (!response.ok) { |
| | throw new Error(`Failed to fetch user ${userId}: ${response.statusText}`); |
| | } |
| | return await response.json(); |
| | } catch (err) { |
| | throw new Error(`Request failed: ${err.message}`); |
| | } |
| | } |
| | |
| | function test1() { |
| | throw Error("Generic error"); |
| | } |
| | |
| | function test2() { |
| | throw Error(); |
| | } |
| | |
| | function test3() { |
| | throw new Error("Generic error"); |
| | } |
| | |
| | function test4() { |
| | throw new Error(); |
| | throw new Error("Pattern should define hostname but found\n" + JSON.stringify(pattern)); |
| | }"#, |
| | |
| | r#" |
| | async function fetchUserData(userId) { |
| | try { |
| | const response = await fetch(`/api/users/${userId}`); |
| | if (!response.ok) { |
| | throw Object.defineProperty(new Error(`Failed to fetch user ${userId}: ${response.statusText}`), "__NEXT_ERROR_CODE", { |
| | value: "E1", |
| | enumerable: false, |
| | configurable: true |
| | }); |
| | } |
| | return await response.json(); |
| | } catch (err) { |
| | throw Object.defineProperty(new Error(`Request failed: ${err.message}`), "__NEXT_ERROR_CODE", { |
| | value: "E2", |
| | enumerable: false, |
| | configurable: true |
| | }); |
| | } |
| | } |
| | function test1() { |
| | throw Object.defineProperty(new Error("Generic error"), "__NEXT_ERROR_CODE", { |
| | value: "E3", |
| | enumerable: false, |
| | configurable: true |
| | }); |
| | } |
| | function test2() { |
| | throw Error(); |
| | } |
| | function test3() { |
| | throw Object.defineProperty(new Error("Generic error"), "__NEXT_ERROR_CODE", { |
| | value: "E3", |
| | enumerable: false, |
| | configurable: true |
| | }); |
| | } |
| | function test4() { |
| | throw new Error(); |
| | throw Object.defineProperty(new Error("Pattern should define hostname but found\n" + JSON.stringify(pattern)), "__NEXT_ERROR_CODE", { |
| | value: "E5", |
| | enumerable: false, |
| | configurable: true |
| | }); |
| | } |
| | "# |
| | ); |
| |
|