diff --git a/crates/perry-codegen/src/codegen/closure.rs b/crates/perry-codegen/src/codegen/closure.rs index e27b02d9ed..183847f136 100644 --- a/crates/perry-codegen/src/codegen/closure.rs +++ b/crates/perry-codegen/src/codegen/closure.rs @@ -314,6 +314,7 @@ pub(super) fn compile_closure( imported_func_return_types: &cross_module.imported_func_return_types, ffi_signatures: &cross_module.ffi_signatures, imported_class_sources: &cross_module.imported_class_sources, + imported_class_original_names: &cross_module.imported_class_original_names, interfaces: &cross_module.interfaces, try_depth: 0, pending_declares: Vec::new(), diff --git a/crates/perry-codegen/src/codegen/entry.rs b/crates/perry-codegen/src/codegen/entry.rs index da5531b359..f65fa089a7 100644 --- a/crates/perry-codegen/src/codegen/entry.rs +++ b/crates/perry-codegen/src/codegen/entry.rs @@ -537,6 +537,7 @@ pub(super) fn compile_module_entry( imported_func_return_types: &cross_module.imported_func_return_types, ffi_signatures: &cross_module.ffi_signatures, imported_class_sources: &cross_module.imported_class_sources, + imported_class_original_names: &cross_module.imported_class_original_names, interfaces: &cross_module.interfaces, try_depth: 0, pending_declares: Vec::new(), @@ -998,6 +999,7 @@ pub(super) fn compile_module_entry( imported_func_return_types: &cross_module.imported_func_return_types, ffi_signatures: &cross_module.ffi_signatures, imported_class_sources: &cross_module.imported_class_sources, + imported_class_original_names: &cross_module.imported_class_original_names, interfaces: &cross_module.interfaces, try_depth: 0, pending_declares: Vec::new(), diff --git a/crates/perry-codegen/src/codegen/function.rs b/crates/perry-codegen/src/codegen/function.rs index 6c0f8fb3b1..2417bfd6ce 100644 --- a/crates/perry-codegen/src/codegen/function.rs +++ b/crates/perry-codegen/src/codegen/function.rs @@ -216,6 +216,7 @@ pub(super) fn compile_function( imported_func_return_types: &cross_module.imported_func_return_types, ffi_signatures: &cross_module.ffi_signatures, imported_class_sources: &cross_module.imported_class_sources, + imported_class_original_names: &cross_module.imported_class_original_names, interfaces: &cross_module.interfaces, try_depth: 0, pending_declares: Vec::new(), diff --git a/crates/perry-codegen/src/codegen/method.rs b/crates/perry-codegen/src/codegen/method.rs index cffe513dae..e7f250058e 100644 --- a/crates/perry-codegen/src/codegen/method.rs +++ b/crates/perry-codegen/src/codegen/method.rs @@ -203,6 +203,7 @@ pub(super) fn compile_method( imported_func_return_types: &cross_module.imported_func_return_types, ffi_signatures: &cross_module.ffi_signatures, imported_class_sources: &cross_module.imported_class_sources, + imported_class_original_names: &cross_module.imported_class_original_names, interfaces: &cross_module.interfaces, try_depth: 0, pending_declares: Vec::new(), @@ -820,6 +821,7 @@ pub(super) fn compile_static_method( imported_func_return_types: &cross_module.imported_func_return_types, ffi_signatures: &cross_module.ffi_signatures, imported_class_sources: &cross_module.imported_class_sources, + imported_class_original_names: &cross_module.imported_class_original_names, interfaces: &cross_module.interfaces, try_depth: 0, pending_declares: Vec::new(), diff --git a/crates/perry-codegen/src/codegen/mod.rs b/crates/perry-codegen/src/codegen/mod.rs index 3e43c72831..e7a435dd2f 100644 --- a/crates/perry-codegen/src/codegen/mod.rs +++ b/crates/perry-codegen/src/codegen/mod.rs @@ -1336,6 +1336,25 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result> } map }, + // Per-module alias → original imported export name. Only renamed named + // imports (`local != imported`) are recorded; this lets `lower_new` + // recover the canonical built-in constructor name when a bundle aliases + // the import (e.g. `import { AsyncLocalStorage as xQ5 }`). See the + // field doc on `CompileOptions::imported_class_original_names`. + imported_class_original_names: { + let mut map: std::collections::HashMap = + std::collections::HashMap::new(); + for import in &hir.imports { + for spec in &import.specifiers { + if let perry_hir::ImportSpecifier::Named { imported, local } = spec { + if local != imported { + map.insert(local.clone(), imported.clone()); + } + } + } + } + map + }, interfaces: hir .interfaces .iter() diff --git a/crates/perry-codegen/src/codegen/opts.rs b/crates/perry-codegen/src/codegen/opts.rs index a39e0febcf..0fb2f8b11d 100644 --- a/crates/perry-codegen/src/codegen/opts.rs +++ b/crates/perry-codegen/src/codegen/opts.rs @@ -737,6 +737,16 @@ pub(crate) struct CrossModuleCtx { /// "Client" arm only fires when the local `Client` was imported from /// "pg" (named or default). See issue #602. pub imported_class_sources: std::collections::HashMap, + /// Per-module mapping: local alias → original imported export name, for + /// named imports where the binding was renamed (`import { AsyncLocalStorage + /// as xQ5 } from "async_hooks"` records `xQ5 -> "AsyncLocalStorage"`). Built + /// once in `compile_module` from `hir.imports`. Lets `lower_new` recover the + /// real export name so the built-in constructor arms in `lower_builtin_new` + /// (keyed on the canonical name like `"AsyncLocalStorage"`) still fire when + /// a minified bundle aliases the import. Without this, `new xQ5()` fell + /// through to the empty-object placeholder and the instance had no + /// `.getStore`/`.run` methods (`TypeError: getStore is not a function`). + pub imported_class_original_names: std::collections::HashMap, /// Issue #655: map from interface name → HIR Interface definition. /// Lets `static_type_of` resolve `obj.field` when `obj` is typed /// against a TS `interface` (not a `class`). The `class_table` diff --git a/crates/perry-codegen/src/expr/mod.rs b/crates/perry-codegen/src/expr/mod.rs index 8a3e42639e..c78711beb6 100644 --- a/crates/perry-codegen/src/expr/mod.rs +++ b/crates/perry-codegen/src/expr/mod.rs @@ -493,6 +493,11 @@ pub(crate) struct FnCtx<'a> { /// Used by `lower_builtin_new` to disambiguate ambiguously-named /// built-in constructors. See issue #602. pub imported_class_sources: &'a std::collections::HashMap, + /// Per-module alias → original imported export name (renamed named imports + /// only). Used by `lower_new` to recover the canonical built-in constructor + /// name when a bundle aliases the import (`import { AsyncLocalStorage as xQ5 + /// }`). See `CompileOptions::imported_class_original_names`. + pub imported_class_original_names: &'a std::collections::HashMap, /// Number of currently-open `try { ... }` blocks at the current /// lowering position. Incremented before lowering a try body, /// decremented after. `Stmt::Return` emits `js_try_end()` this many diff --git a/crates/perry-codegen/src/lower_call/new.rs b/crates/perry-codegen/src/lower_call/new.rs index 3e8daa4624..fcce72747f 100644 --- a/crates/perry-codegen/src/lower_call/new.rs +++ b/crates/perry-codegen/src/lower_call/new.rs @@ -360,6 +360,24 @@ pub(crate) fn lower_new(ctx: &mut FnCtx<'_>, class_name: &str, args: &[Expr]) -> if let Some(val) = lower_builtin_new(ctx, class_name, args)? { return Ok(val); } + // Aliased built-in import: a minified bundle renames a node built-in + // constructor (`import { AsyncLocalStorage as xQ5 } from "async_hooks"; + // new xQ5()`). The syntactic callee is the alias `xQ5`, so the + // canonical-name arms in `lower_builtin_new` (keyed on + // `"AsyncLocalStorage"`) never fired and `new xQ5()` fell through to the + // empty-object placeholder — the instance had no `.run`/`.getStore`, so + // `xQ5().getStore()` threw `TypeError: getStore is not a function`. + // Recover the original export name and retry. The alias is only present + // here when it was NOT already a user-defined class (the enclosing + // `!ctx.classes.contains_key(class_name)` guard), so a renamed import + // can't shadow a real local class. + if let Some(original) = ctx.imported_class_original_names.get(class_name).cloned() { + if original != class_name { + if let Some(val) = lower_builtin_new(ctx, &original, args)? { + return Ok(val); + } + } + } } // Local class alias rerouting: `let C = SomeClass; new C()` lowers diff --git a/crates/perry/tests/aliased_native_class_import.rs b/crates/perry/tests/aliased_native_class_import.rs index 9a9857289f..a60cfe30ca 100644 --- a/crates/perry/tests/aliased_native_class_import.rs +++ b/crates/perry/tests/aliased_native_class_import.rs @@ -214,6 +214,69 @@ console.log(typeof s.connect, typeof s.write, typeof s.end); assert_eq!(aliased, "function function function\n"); } +/// Aliased `async_hooks.AsyncLocalStorage`: a minified bundle renames the +/// import (`import { AsyncLocalStorage as xQ5 }`) and later does `new xQ5()`. +/// The syntactic `new`-callee is the alias `xQ5`, so the codegen built-in +/// constructor arm (keyed on the canonical name `"AsyncLocalStorage"`) did not +/// fire and the instance fell through to the empty-object placeholder with no +/// methods — `xQ5().getStore()` threw `TypeError: getStore is not a function`. +/// Exercises the exact `run(store, cb)` / `getStore()` round-trip that a CLI's +/// `doctor`-style context plumbing relies on. +#[test] +fn aliased_async_local_storage_run_getstore_round_trip() { + let dir = tempfile::tempdir().expect("tempdir"); + let stdout = compile_and_run( + dir.path(), + r#" +import { AsyncLocalStorage as xQ5 } from "async_hooks"; +const als: any = new xQ5(); +console.log("getStore type:", typeof als.getStore); +console.log("outside:", als.getStore()); +const inside = als.run({ cwd: "/work" }, () => als.getStore().cwd); +console.log("inside:", inside); +console.log("after:", als.getStore()); +"#, + ); + assert_eq!( + stdout, + "getStore type: function\n\ + outside: undefined\n\ + inside: /work\n\ + after: undefined\n" + ); +} + +/// The aliased `async_hooks.AsyncLocalStorage` path must match the un-aliased +/// path byte-for-byte (alias resolution == canonical lowering). +#[test] +fn aliased_async_local_storage_matches_unaliased() { + let prog = |import_line: &str, ctor: &str| { + format!( + "{import_line}\nconst als: any = new {ctor}();\n\ + const r = als.run({{ id: 7 }}, () => als.getStore().id);\n\ + console.log(typeof als.getStore, typeof als.run, r, als.getStore());\n" + ) + }; + let dir = tempfile::tempdir().expect("tempdir"); + let aliased = compile_and_run( + dir.path(), + &prog( + "import { AsyncLocalStorage as Q9 } from \"node:async_hooks\";", + "Q9", + ), + ); + let dir2 = tempfile::tempdir().expect("tempdir"); + let unaliased = compile_and_run( + dir2.path(), + &prog( + "import { AsyncLocalStorage } from \"node:async_hooks\";", + "AsyncLocalStorage", + ), + ); + assert_eq!(aliased, unaliased); + assert_eq!(aliased, "function function 7 undefined\n"); +} + /// A non-native user import alias must NOT be treated as a native class — the /// fix must not over-trigger native handling on ordinary user modules. #[test]