From 286aa5488a2ae882e9e60f70b1cd95b08d8c4bd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Tue, 22 Sep 2026 18:20:41 +0200 Subject: [PATCH 1/2] fix: preserve nested CJS constructor function names --- .../lower_decl/body_stmt/nested_fn_decl.rs | 5 ++ .../tests/issue_10702_cjs_function_name.rs | 66 +++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 crates/perry/tests/issue_10702_cjs_function_name.rs diff --git a/crates/perry-hir/src/lower_decl/body_stmt/nested_fn_decl.rs b/crates/perry-hir/src/lower_decl/body_stmt/nested_fn_decl.rs index 230a46e4e3..096f106ee5 100644 --- a/crates/perry-hir/src/lower_decl/body_stmt/nested_fn_decl.rs +++ b/crates/perry-hir/src/lower_decl/body_stmt/nested_fn_decl.rs @@ -284,6 +284,11 @@ pub(super) fn lower_nested_fn_decl( // tag has to be applied here too. ctx.function_valued_locals.insert(local_id); + // Nested declarations become inline closures rather than entries in + // `module.functions`. Preserve the declared name for function.name; + // codegen cannot infer it from a Let inside another closure's body. + ctx.closure_display_names.insert(func_id, func_name.clone()); + let closure = Expr::Closure { func_id, params, diff --git a/crates/perry/tests/issue_10702_cjs_function_name.rs b/crates/perry/tests/issue_10702_cjs_function_name.rs new file mode 100644 index 0000000000..1f49d3aa17 --- /dev/null +++ b/crates/perry/tests/issue_10702_cjs_function_name.rs @@ -0,0 +1,66 @@ +//! A CJS default export preserves the name of its function constructor. + +use std::path::PathBuf; +use std::process::Command; + +#[test] +fn imported_cjs_function_constructor_keeps_its_name() { + let dir = tempfile::tempdir().expect("tempdir"); + let entry = dir.path().join("main.ts"); + let library = dir.path().join("lib.cjs"); + let binary = dir.path().join("main_bin"); + std::fs::write( + &library, + r#" +function clone() { + function Ledger(value) { this.value = value; } + Ledger.prototype = { constructor: Ledger, getValue() { return this.value; } }; + return Ledger; +} +module.exports = clone(); +"#, + ) + .expect("write CJS library"); + std::fs::write( + &entry, + r#" +import Ledger from "./lib.cjs"; +const value = new Ledger(5); +console.log("NAMES", Ledger.name, value.constructor.name); +console.log("VALUE", value instanceof Ledger, value.getValue()); +"#, + ) + .expect("write entry"); + + let compiler = PathBuf::from(env!("CARGO_BIN_EXE_perry")); + let compile = Command::new(compiler) + .current_dir(dir.path()) + .arg("compile") + .arg(&entry) + .arg("-o") + .arg(&binary) + .arg("--no-cache") + .output() + .expect("compile fixture"); + assert!( + compile.status.success(), + "compile failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&compile.stdout), + String::from_utf8_lossy(&compile.stderr) + ); + + let run = Command::new(binary) + .current_dir(dir.path()) + .output() + .expect("run fixture"); + assert!( + run.status.success(), + "fixture failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&run.stdout), + String::from_utf8_lossy(&run.stderr) + ); + assert_eq!( + String::from_utf8_lossy(&run.stdout), + "NAMES Ledger Ledger\nVALUE true 5\n" + ); +} From e043e7890cea1f8359ed037fddca79a7cbfdce34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Tue, 22 Sep 2026 18:21:09 +0200 Subject: [PATCH 2/2] docs: note CJS function name fix --- changelog.d/11009-cjs-function-names.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 changelog.d/11009-cjs-function-names.md diff --git a/changelog.d/11009-cjs-function-names.md b/changelog.d/11009-cjs-function-names.md new file mode 100644 index 0000000000..ebe51fceb9 --- /dev/null +++ b/changelog.d/11009-cjs-function-names.md @@ -0,0 +1,3 @@ +### Fixed + +- Preserve the declared `.name` of nested function constructors exported from CommonJS modules, including values built by a factory and inspected through `constructor.name`.