diff --git a/crates/perry-hir/src/lower/expr_call/globals.rs b/crates/perry-hir/src/lower/expr_call/globals.rs index 7bf19cc748..2ea9e6bd1e 100644 --- a/crates/perry-hir/src/lower/expr_call/globals.rs +++ b/crates/perry-hir/src/lower/expr_call/globals.rs @@ -484,7 +484,13 @@ pub(super) fn try_global_builtins( } // Check if this is a named import from child_process (e.g., execSync, spawnSync) - if let Some((module_name, _method)) = ctx.lookup_native_module(func_name) { + if let Some((module_name, method)) = ctx.lookup_native_module(func_name) { + // Resolve aliased named imports of native-module functions to their + // EXPORTED name (`import { join as p } from "path"` → "join") so the + // per-module `match func_name` arms below match. Without this, an + // aliased local (`p`) matched no arm and fell through to a generic + // path that evaluated to `undefined` — e.g. `p(home, ".x").normalize()`. + let func_name = method.unwrap_or(func_name); if module_name == "child_process" { match func_name { "execSync" if !args.is_empty() => { diff --git a/crates/perry/tests/issue_aliased_native_module_function_import.rs b/crates/perry/tests/issue_aliased_native_module_function_import.rs new file mode 100644 index 0000000000..f8e709d460 --- /dev/null +++ b/crates/perry/tests/issue_aliased_native_module_function_import.rs @@ -0,0 +1,84 @@ +//! Aliased named imports of native-module functions must resolve to the +//! exported function, not the local alias. +//! +//! `import { join as p } from "path"; p(a, b)` returned `undefined` — the +//! native-module function-call lowering matched the LOCAL binding name (`p`) +//! against each module's known export names (`match func_name { "join" => … }`), +//! so an aliased local never matched and fell through to a generic path that +//! evaluated to `undefined`. Any later use then failed — e.g. the minified +//! config-dir shape `(process.env.X ?? p(home(), ".dir")).normalize("NFC")` +//! threw `Cannot read properties of undefined (reading 'normalize')`. +//! Unaliased `import { join }` worked only because local == export. + +use std::path::PathBuf; +use std::process::Command; + +fn perry_bin() -> PathBuf { + PathBuf::from(env!("CARGO_BIN_EXE_perry")) +} + +fn compile_and_run(dir: &std::path::Path, entry: &std::path::Path) -> (bool, String) { + let output = dir.join("main_bin"); + let compile = Command::new(perry_bin()) + .current_dir(dir) + .arg("compile") + .arg(entry) + .arg("-o") + .arg(&output) + .output() + .expect("run perry compile"); + assert!( + compile.status.success(), + "perry compile failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&compile.stdout), + String::from_utf8_lossy(&compile.stderr) + ); + let run = Command::new(&output).output().expect("run compiled binary"); + ( + run.status.success(), + String::from_utf8_lossy(&run.stdout).to_string(), + ) +} + +#[test] +fn aliased_native_module_function_imports_resolve() { + let dir = tempfile::tempdir().expect("tempdir"); + let entry = dir.path().join("main.ts"); + std::fs::write( + &entry, + r#" +import { join as p, dirname as d, resolve as r } from "path" +import { homedir as h } from "os" + +// Aliased path functions resolve to the real exports (not the local alias). +console.log("join:", p("/a", "b", "c")) +console.log("join-dot:", p("/a", ".dir")) +console.log("dirname:", d("/a/b/c")) +console.log("resolve-abs:", r("/a", "/b")) +console.log("homedir-type:", typeof h) + +// The minified config-dir shape that surfaced the bug. +const cfg: any = (process.env.DEFINITELY_UNSET_CFG_DIR ?? p(h(), ".dir")).normalize("NFC") +console.log("cfg endsWith /.dir:", typeof cfg === "string" && cfg.endsWith("/.dir")) +console.log("DONE") +"#, + ) + .expect("write entry"); + + let (ok, stdout) = compile_and_run(dir.path(), &entry); + assert!(ok, "binary failed\nstdout:\n{stdout}"); + for needle in [ + "join: /a/b/c", + "join-dot: /a/.dir", + "dirname: /a/b", + "resolve-abs: /b", + "homedir-type: function", + "cfg endsWith /.dir: true", + "DONE", + ] { + assert!( + stdout.contains(needle), + "expected `{needle}` in output:\n{stdout}" + ); + } +}