From ba681076da8a51acdb51aae7724db36ace3e9492 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Donny/=EA=B0=95=EB=8F=99=EC=9C=A4?= Date: Wed, 24 Jul 2024 21:01:59 +0900 Subject: [PATCH] feat(next-swc): Add a transform adding function names (#68056) ### What? Implements a transform that adds readable names to anonymous function expressions in react component functions. Adding `{ debugFunctionName: true }` to swcOptions will do the trick. ### Why? x-ref: https://vercel.slack.com/archives/C04TR3U872A/p1721702161121619 ### How? --- .../src/chain_transforms.rs | 4 + .../src/transforms/debug_fn_name.rs | 185 ++++++++++++++++++ .../src/transforms/mod.rs | 1 + .../next-custom-transforms/tests/fixture.rs | 22 +++ .../anonymous-component/input.js | 6 + .../anonymous-component/output.js | 14 ++ .../debug-fn-name/composite-hook/input.js | 6 + .../debug-fn-name/composite-hook/output.js | 14 ++ .../debug-fn-name/export-default/input.js | 6 + .../debug-fn-name/export-default/output.js | 14 ++ .../tests/fixture/debug-fn-name/hoc/input.js | 6 + .../tests/fixture/debug-fn-name/hoc/output.js | 14 ++ .../fixture/debug-fn-name/lone-memo/input.js | 1 + .../fixture/debug-fn-name/lone-memo/output.js | 1 + .../fixture/debug-fn-name/normal/input.js | 6 + .../fixture/debug-fn-name/normal/output.js | 14 ++ .../debug-fn-name/outlined-functions/input.js | 4 + .../outlined-functions/output.js | 4 + crates/next-custom-transforms/tests/full.rs | 1 + 19 files changed, 323 insertions(+) create mode 100644 crates/next-custom-transforms/src/transforms/debug_fn_name.rs create mode 100644 crates/next-custom-transforms/tests/fixture/debug-fn-name/anonymous-component/input.js create mode 100644 crates/next-custom-transforms/tests/fixture/debug-fn-name/anonymous-component/output.js create mode 100644 crates/next-custom-transforms/tests/fixture/debug-fn-name/composite-hook/input.js create mode 100644 crates/next-custom-transforms/tests/fixture/debug-fn-name/composite-hook/output.js create mode 100644 crates/next-custom-transforms/tests/fixture/debug-fn-name/export-default/input.js create mode 100644 crates/next-custom-transforms/tests/fixture/debug-fn-name/export-default/output.js create mode 100644 crates/next-custom-transforms/tests/fixture/debug-fn-name/hoc/input.js create mode 100644 crates/next-custom-transforms/tests/fixture/debug-fn-name/hoc/output.js create mode 100644 crates/next-custom-transforms/tests/fixture/debug-fn-name/lone-memo/input.js create mode 100644 crates/next-custom-transforms/tests/fixture/debug-fn-name/lone-memo/output.js create mode 100644 crates/next-custom-transforms/tests/fixture/debug-fn-name/normal/input.js create mode 100644 crates/next-custom-transforms/tests/fixture/debug-fn-name/normal/output.js create mode 100644 crates/next-custom-transforms/tests/fixture/debug-fn-name/outlined-functions/input.js create mode 100644 crates/next-custom-transforms/tests/fixture/debug-fn-name/outlined-functions/output.js diff --git a/crates/next-custom-transforms/src/chain_transforms.rs b/crates/next-custom-transforms/src/chain_transforms.rs index ebcdb62bd0ff5..f2576f90165f5 100644 --- a/crates/next-custom-transforms/src/chain_transforms.rs +++ b/crates/next-custom-transforms/src/chain_transforms.rs @@ -109,6 +109,9 @@ pub struct TransformOptions { #[serde(default)] pub optimize_server_react: Option, + + #[serde(default)] + pub debug_function_name: bool, } pub fn custom_before_pass<'a, C>( @@ -297,6 +300,7 @@ where }, None => Either::Right(noop()), }, + Optional::new(crate::transforms::debug_fn_name::debug_fn_name(), opts.debug_function_name), as_folder(crate::transforms::pure::pure_magic(comments)), ) } diff --git a/crates/next-custom-transforms/src/transforms/debug_fn_name.rs b/crates/next-custom-transforms/src/transforms/debug_fn_name.rs new file mode 100644 index 0000000000000..769e3fec585a9 --- /dev/null +++ b/crates/next-custom-transforms/src/transforms/debug_fn_name.rs @@ -0,0 +1,185 @@ +use std::fmt::Write; + +use swc_core::{ + atoms::Atom, + common::{util::take::Take, DUMMY_SP}, + ecma::{ + ast::{ + CallExpr, Callee, ExportDefaultExpr, Expr, FnDecl, FnExpr, KeyValueProp, MemberProp, + ObjectLit, PropOrSpread, VarDeclarator, + }, + utils::ExprFactory, + visit::{as_folder, Fold, VisitMut, VisitMutWith}, + }, +}; + +pub fn debug_fn_name() -> impl VisitMut + Fold { + as_folder(DebugFnName::default()) +} + +#[derive(Default)] +struct DebugFnName { + path: String, + in_target: bool, + in_var_target: bool, + in_default_export: bool, +} + +impl VisitMut for DebugFnName { + fn visit_mut_call_expr(&mut self, n: &mut CallExpr) { + if self.in_var_target || (self.path.is_empty() && !self.in_default_export) { + n.visit_mut_children_with(self); + return; + } + + if let Some(target) = is_target_callee(&n.callee) { + let old_in_target = self.in_target; + self.in_target = true; + let orig_len = self.path.len(); + if !self.path.is_empty() { + self.path.push('.'); + } + self.path.push_str(&target); + + n.visit_mut_children_with(self); + + self.path.truncate(orig_len); + self.in_target = old_in_target; + } else { + n.visit_mut_children_with(self); + } + } + + fn visit_mut_export_default_expr(&mut self, n: &mut ExportDefaultExpr) { + let old_in_default_export = self.in_default_export; + self.in_default_export = true; + + n.visit_mut_children_with(self); + + self.in_default_export = old_in_default_export; + } + + fn visit_mut_expr(&mut self, n: &mut Expr) { + n.visit_mut_children_with(self); + + if self.in_target { + match n { + Expr::Arrow(..) | Expr::Fn(FnExpr { ident: None, .. }) => { + // useLayoutEffect(() => ...); + // + // becomes + // + // + // useLayoutEffect({'MyComponent.useLayoutEffect': () => + // ...}['MyComponent.useLayoutEffect']); + + let orig = n.take(); + let key = Atom::from(&*self.path); + + *n = Expr::Object(ObjectLit { + span: DUMMY_SP, + props: vec![PropOrSpread::Prop(Box::new( + swc_core::ecma::ast::Prop::KeyValue(KeyValueProp { + key: swc_core::ecma::ast::PropName::Str(key.clone().into()), + value: Box::new(orig), + }), + ))], + }) + .computed_member(key) + .into(); + } + + _ => {} + } + } + } + + fn visit_mut_fn_decl(&mut self, n: &mut FnDecl) { + let orig_len = self.path.len(); + if !self.path.is_empty() { + self.path.push('.'); + } + self.path.push_str(n.ident.sym.as_str()); + + n.visit_mut_children_with(self); + + self.path.truncate(orig_len); + } + + fn visit_mut_var_declarator(&mut self, n: &mut VarDeclarator) { + if let Some(Expr::Call(call)) = n.init.as_deref() { + let name = is_target_callee(&call.callee).and_then(|target| { + let name = n.name.as_ident()?; + + Some((name.sym.clone(), target)) + }); + + if let Some((name, target)) = name { + let old_in_var_target = self.in_var_target; + self.in_var_target = true; + + let old_in_target = self.in_target; + self.in_target = true; + let orig_len = self.path.len(); + if !self.path.is_empty() { + self.path.push('.'); + } + let _ = write!(self.path, "{target}({name})"); + + n.visit_mut_children_with(self); + + self.path.truncate(orig_len); + self.in_target = old_in_target; + self.in_var_target = old_in_var_target; + return; + } + } + + if let Some(Expr::Arrow(..) | Expr::Fn(FnExpr { ident: None, .. }) | Expr::Call(..)) = + n.init.as_deref() + { + let name = n.name.as_ident(); + + if let Some(name) = name { + let orig_len = self.path.len(); + if !self.path.is_empty() { + self.path.push('.'); + } + self.path.push_str(name.sym.as_str()); + + n.visit_mut_children_with(self); + + self.path.truncate(orig_len); + return; + } + } + + n.visit_mut_children_with(self); + } +} + +fn is_target_callee(e: &Callee) -> Option { + match e { + Callee::Expr(e) => match &**e { + Expr::Ident(i) => { + if i.sym.starts_with("use") { + Some(i.sym.clone()) + } else { + None + } + } + Expr::Member(me) => match &me.prop { + MemberProp::Ident(i) => { + if i.sym.starts_with("use") { + Some(i.sym.clone()) + } else { + None + } + } + _ => None, + }, + _ => None, + }, + _ => None, + } +} diff --git a/crates/next-custom-transforms/src/transforms/mod.rs b/crates/next-custom-transforms/src/transforms/mod.rs index 92f7fcd165a0c..a3acb4ce0a389 100644 --- a/crates/next-custom-transforms/src/transforms/mod.rs +++ b/crates/next-custom-transforms/src/transforms/mod.rs @@ -1,6 +1,7 @@ pub mod amp_attributes; pub mod cjs_finder; pub mod cjs_optimizer; +pub mod debug_fn_name; pub mod disallow_re_export_all_in_page; pub mod dynamic; pub mod fonts; diff --git a/crates/next-custom-transforms/tests/fixture.rs b/crates/next-custom-transforms/tests/fixture.rs index 72cac88b8563a..945758906e223 100644 --- a/crates/next-custom-transforms/tests/fixture.rs +++ b/crates/next-custom-transforms/tests/fixture.rs @@ -6,6 +6,7 @@ use std::{ use next_custom_transforms::transforms::{ amp_attributes::amp_attributes, cjs_optimizer::cjs_optimizer, + debug_fn_name::debug_fn_name, dynamic::{next_dynamic, NextDynamicMode}, fonts::{next_font_loaders, Config as FontLoaderConfig}, named_import_transform::named_import_transform, @@ -630,3 +631,24 @@ fn next_transform_strip_page_exports_fixture_default(output: PathBuf) { run_stip_page_exports_test(&input, &output, ExportFilter::StripDataExports); } + +#[fixture("tests/fixture/debug-fn-name/**/input.js")] +fn test_debug_name(input: PathBuf) { + let output = input.parent().unwrap().join("output.js"); + + test_fixture( + syntax(), + &|_| { + let top_level_mark = Mark::fresh(Mark::root()); + let unresolved_mark = Mark::fresh(Mark::root()); + + chain!( + swc_core::ecma::transforms::base::resolver(unresolved_mark, top_level_mark, true), + debug_fn_name() + ) + }, + &input, + &output, + Default::default(), + ); +} diff --git a/crates/next-custom-transforms/tests/fixture/debug-fn-name/anonymous-component/input.js b/crates/next-custom-transforms/tests/fixture/debug-fn-name/anonymous-component/input.js new file mode 100644 index 0000000000000..fd558273f211a --- /dev/null +++ b/crates/next-custom-transforms/tests/fixture/debug-fn-name/anonymous-component/input.js @@ -0,0 +1,6 @@ +const Component = () => { + useLayoutEffect(() => {}) + useEffect(() => {}) + const onClick = useCallback(() => []) + const computed = useMemo(() => {}) +} diff --git a/crates/next-custom-transforms/tests/fixture/debug-fn-name/anonymous-component/output.js b/crates/next-custom-transforms/tests/fixture/debug-fn-name/anonymous-component/output.js new file mode 100644 index 0000000000000..cd0b8ba7c6212 --- /dev/null +++ b/crates/next-custom-transforms/tests/fixture/debug-fn-name/anonymous-component/output.js @@ -0,0 +1,14 @@ +const Component = ()=>{ + useLayoutEffect({ + "Component.useLayoutEffect": ()=>{} + }["Component.useLayoutEffect"]); + useEffect({ + "Component.useEffect": ()=>{} + }["Component.useEffect"]); + const onClick = useCallback({ + "Component.useCallback(onClick)": ()=>[] + }["Component.useCallback(onClick)"]); + const computed = useMemo({ + "Component.useMemo(computed)": ()=>{} + }["Component.useMemo(computed)"]); +}; diff --git a/crates/next-custom-transforms/tests/fixture/debug-fn-name/composite-hook/input.js b/crates/next-custom-transforms/tests/fixture/debug-fn-name/composite-hook/input.js new file mode 100644 index 0000000000000..260b2578123e5 --- /dev/null +++ b/crates/next-custom-transforms/tests/fixture/debug-fn-name/composite-hook/input.js @@ -0,0 +1,6 @@ +function useSomething() { + useLayoutEffect(() => {}) + useEffect(() => {}) + const onClick = useCallback(() => []) + const computed = useMemo(() => {}) +} diff --git a/crates/next-custom-transforms/tests/fixture/debug-fn-name/composite-hook/output.js b/crates/next-custom-transforms/tests/fixture/debug-fn-name/composite-hook/output.js new file mode 100644 index 0000000000000..dff6ea08cdc61 --- /dev/null +++ b/crates/next-custom-transforms/tests/fixture/debug-fn-name/composite-hook/output.js @@ -0,0 +1,14 @@ +function useSomething() { + useLayoutEffect({ + "useSomething.useLayoutEffect": ()=>{} + }["useSomething.useLayoutEffect"]); + useEffect({ + "useSomething.useEffect": ()=>{} + }["useSomething.useEffect"]); + const onClick = useCallback({ + "useSomething.useCallback(onClick)": ()=>[] + }["useSomething.useCallback(onClick)"]); + const computed = useMemo({ + "useSomething.useMemo(computed)": ()=>{} + }["useSomething.useMemo(computed)"]); +} diff --git a/crates/next-custom-transforms/tests/fixture/debug-fn-name/export-default/input.js b/crates/next-custom-transforms/tests/fixture/debug-fn-name/export-default/input.js new file mode 100644 index 0000000000000..cd1cf9d7969e6 --- /dev/null +++ b/crates/next-custom-transforms/tests/fixture/debug-fn-name/export-default/input.js @@ -0,0 +1,6 @@ +export default () => { + useLayoutEffect(() => {}) + useEffect(() => {}) + const onClick = useCallback(() => []) + const computed = useMemo(() => {}) +} diff --git a/crates/next-custom-transforms/tests/fixture/debug-fn-name/export-default/output.js b/crates/next-custom-transforms/tests/fixture/debug-fn-name/export-default/output.js new file mode 100644 index 0000000000000..4323650b80467 --- /dev/null +++ b/crates/next-custom-transforms/tests/fixture/debug-fn-name/export-default/output.js @@ -0,0 +1,14 @@ +export default (()=>{ + useLayoutEffect({ + "useLayoutEffect": ()=>{} + }["useLayoutEffect"]); + useEffect({ + "useEffect": ()=>{} + }["useEffect"]); + const onClick = useCallback({ + "useCallback(onClick)": ()=>[] + }["useCallback(onClick)"]); + const computed = useMemo({ + "useMemo(computed)": ()=>{} + }["useMemo(computed)"]); +}); diff --git a/crates/next-custom-transforms/tests/fixture/debug-fn-name/hoc/input.js b/crates/next-custom-transforms/tests/fixture/debug-fn-name/hoc/input.js new file mode 100644 index 0000000000000..bcc7d088b0f47 --- /dev/null +++ b/crates/next-custom-transforms/tests/fixture/debug-fn-name/hoc/input.js @@ -0,0 +1,6 @@ +const Component = someHoC(() => { + useLayoutEffect(() => {}) + useEffect(() => {}) + const onClick = useCallback(() => []) + const computed = useMemo(() => {}) +}) diff --git a/crates/next-custom-transforms/tests/fixture/debug-fn-name/hoc/output.js b/crates/next-custom-transforms/tests/fixture/debug-fn-name/hoc/output.js new file mode 100644 index 0000000000000..0a84ae7849d71 --- /dev/null +++ b/crates/next-custom-transforms/tests/fixture/debug-fn-name/hoc/output.js @@ -0,0 +1,14 @@ +const Component = someHoC(()=>{ + useLayoutEffect({ + "Component.useLayoutEffect": ()=>{} + }["Component.useLayoutEffect"]); + useEffect({ + "Component.useEffect": ()=>{} + }["Component.useEffect"]); + const onClick = useCallback({ + "Component.useCallback(onClick)": ()=>[] + }["Component.useCallback(onClick)"]); + const computed = useMemo({ + "Component.useMemo(computed)": ()=>{} + }["Component.useMemo(computed)"]); +}); diff --git a/crates/next-custom-transforms/tests/fixture/debug-fn-name/lone-memo/input.js b/crates/next-custom-transforms/tests/fixture/debug-fn-name/lone-memo/input.js new file mode 100644 index 0000000000000..5fa6eb48a710f --- /dev/null +++ b/crates/next-custom-transforms/tests/fixture/debug-fn-name/lone-memo/input.js @@ -0,0 +1 @@ +useMemo(() => {}) diff --git a/crates/next-custom-transforms/tests/fixture/debug-fn-name/lone-memo/output.js b/crates/next-custom-transforms/tests/fixture/debug-fn-name/lone-memo/output.js new file mode 100644 index 0000000000000..d086de46a51d5 --- /dev/null +++ b/crates/next-custom-transforms/tests/fixture/debug-fn-name/lone-memo/output.js @@ -0,0 +1 @@ +useMemo(()=>{}); diff --git a/crates/next-custom-transforms/tests/fixture/debug-fn-name/normal/input.js b/crates/next-custom-transforms/tests/fixture/debug-fn-name/normal/input.js new file mode 100644 index 0000000000000..0100bc58cd6e7 --- /dev/null +++ b/crates/next-custom-transforms/tests/fixture/debug-fn-name/normal/input.js @@ -0,0 +1,6 @@ +function MyComponent() { + useLayoutEffect(() => {}) + useEffect(() => {}) + const onClick = useCallback(() => []) + const computed = useMemo(() => {}) +} diff --git a/crates/next-custom-transforms/tests/fixture/debug-fn-name/normal/output.js b/crates/next-custom-transforms/tests/fixture/debug-fn-name/normal/output.js new file mode 100644 index 0000000000000..8a88618f87afc --- /dev/null +++ b/crates/next-custom-transforms/tests/fixture/debug-fn-name/normal/output.js @@ -0,0 +1,14 @@ +function MyComponent() { + useLayoutEffect({ + "MyComponent.useLayoutEffect": ()=>{} + }["MyComponent.useLayoutEffect"]); + useEffect({ + "MyComponent.useEffect": ()=>{} + }["MyComponent.useEffect"]); + const onClick = useCallback({ + "MyComponent.useCallback(onClick)": ()=>[] + }["MyComponent.useCallback(onClick)"]); + const computed = useMemo({ + "MyComponent.useMemo(computed)": ()=>{} + }["MyComponent.useMemo(computed)"]); +} diff --git a/crates/next-custom-transforms/tests/fixture/debug-fn-name/outlined-functions/input.js b/crates/next-custom-transforms/tests/fixture/debug-fn-name/outlined-functions/input.js new file mode 100644 index 0000000000000..9252f7481b06f --- /dev/null +++ b/crates/next-custom-transforms/tests/fixture/debug-fn-name/outlined-functions/input.js @@ -0,0 +1,4 @@ +function C() { + function handleClick() {} + const onClick = useCallback(handleClick, []) +} diff --git a/crates/next-custom-transforms/tests/fixture/debug-fn-name/outlined-functions/output.js b/crates/next-custom-transforms/tests/fixture/debug-fn-name/outlined-functions/output.js new file mode 100644 index 0000000000000..239a7688cda50 --- /dev/null +++ b/crates/next-custom-transforms/tests/fixture/debug-fn-name/outlined-functions/output.js @@ -0,0 +1,4 @@ +function C() { + function handleClick() {} + const onClick = useCallback(handleClick, []); +} diff --git a/crates/next-custom-transforms/tests/full.rs b/crates/next-custom-transforms/tests/full.rs index a0a2bb784b777..155d17313ce7d 100644 --- a/crates/next-custom-transforms/tests/full.rs +++ b/crates/next-custom-transforms/tests/full.rs @@ -81,6 +81,7 @@ fn test(input: &Path, minify: bool) { optimize_barrel_exports: None, optimize_server_react: None, prefer_esm: false, + debug_function_name: false, }; let unresolved_mark = Mark::new();