From b7081db04a87627e7265c9ba35f43be6e038abca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 21 Sep 2026 10:56:40 +0200 Subject: [PATCH] fix(compile): resolve JSON requires from ESM module paths --- changelog.d/10758-esm-static-json-require.md | 4 ++ .../src/commands/compile/collect_modules.rs | 2 +- .../static_require_transform.rs | 56 ++++++++++++++++++- .../tests/source_graph_export_regressions.rs | 2 + .../issue_10758.rs | 30 ++++++++++ 5 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 changelog.d/10758-esm-static-json-require.md create mode 100644 crates/perry/tests/source_graph_export_regressions/issue_10758.rs diff --git a/changelog.d/10758-esm-static-json-require.md b/changelog.d/10758-esm-static-json-require.md new file mode 100644 index 0000000000..d63c4402b9 --- /dev/null +++ b/changelog.d/10758-esm-static-json-require.md @@ -0,0 +1,4 @@ +Resolve static relative JSON `require()` calls in ESM-shaped TypeScript from +the requiring module's directory. Packages such as MongoDB can now read their +own `package.json` when compiled from source instead of resolving the path from +the process entry and throwing `MODULE_NOT_FOUND` during connection setup. diff --git a/crates/perry/src/commands/compile/collect_modules.rs b/crates/perry/src/commands/compile/collect_modules.rs index 0a9474d9fd..8f0d359443 100644 --- a/crates/perry/src/commands/compile/collect_modules.rs +++ b/crates/perry/src/commands/compile/collect_modules.rs @@ -457,8 +457,8 @@ fn collect_module_one( &ctx.compile_packages, canonical.parent().unwrap_or_else(|| Path::new(".")), ctx.bunfs_root.as_deref(), + !was_cjs_wrapped, ); - // #8547: a builtin reached through `require("http")` never appears in the // ESM import walk below, so `needs_stdlib` stayed false, the link came out // runtime-only, `perry-stdlib`'s dispatch init never ran, and every diff --git a/crates/perry/src/commands/compile/collect_modules/static_require_transform.rs b/crates/perry/src/commands/compile/collect_modules/static_require_transform.rs index 742e1a4f07..67a61c1169 100644 --- a/crates/perry/src/commands/compile/collect_modules/static_require_transform.rs +++ b/crates/perry/src/commands/compile/collect_modules/static_require_transform.rs @@ -15,7 +15,7 @@ fn transform_static_literal_requires( compile_packages: &HashSet, module_dir: &Path, ) -> String { - transform_static_literal_requires_with_bunfs(source, compile_packages, module_dir, None) + transform_static_literal_requires_with_bunfs(source, compile_packages, module_dir, None, false) } pub(super) fn transform_static_literal_requires_with_bunfs( @@ -23,6 +23,7 @@ pub(super) fn transform_static_literal_requires_with_bunfs( compile_packages: &HashSet, module_dir: &Path, bunfs_root: Option<&Path>, + replace_json_requires: bool, ) -> String { let create_require_aliases = collect_create_require_aliases(source); let mut require_aliases = @@ -140,6 +141,25 @@ pub(super) fn transform_static_literal_requires_with_bunfs( if let Some(target) = require_target.as_ref() { let is_native_addon = target.extension().and_then(|extension| extension.to_str()) == Some("node"); + // #10758: an ESM-shaped TypeScript source can still contain a + // literal `require("./data.json")` (mongodb reads its own + // package.json this way). Unlike a CJS-wrapped source it has + // no per-module `require` shim, so leaving the call in place + // resolves it relative to the process entry instead of the + // importing module. The JSON loader's default export is the + // same cached object a static require returns, so use that + // binding directly for unwrapped ESM modules. + if replace_json_requires + && target.extension().and_then(|extension| extension.to_str()) == Some("json") + { + let binding = unique_temp_name(source, &mut next_id); + imports.push(format!( + "import {binding} from {:?};", + target.to_string_lossy() + )); + replacements.push((full.start(), full.end(), binding)); + continue; + } if !matches!( target.extension().and_then(|e| e.to_str()), Some("ts" | "tsx" | "mts" | "cts" | "js" | "mjs") @@ -763,6 +783,40 @@ console.log(require("./local").value); assert!(got.contains("console.log(__perry_static_require_0.value);")); } + #[test] + fn replaces_resolved_json_require_in_unwrapped_esm_module() { + let dir = tempfile::tempdir().expect("tempdir"); + let module_dir = dir.path().join("src/cmap/handshake"); + std::fs::create_dir_all(&module_dir).expect("create nested module dir"); + let package_json = dir.path().join("package.json"); + std::fs::write(&package_json, r#"{"version":"7.5.0"}"#).expect("write package.json"); + let source = r#" +import { marker } from "./marker"; +export const version = require("../../../package.json").version + marker; +"#; + + let got = transform_static_literal_requires_with_bunfs( + source, + &HashSet::new(), + &module_dir, + None, + true, + ); + + assert!( + got.contains(&format!( + "import __perry_static_require_0 from {:?};", + package_json + .canonicalize() + .expect("canonicalize package.json") + .to_string_lossy() + )), + "got:\n{got}" + ); + assert!(got.contains("export const version = __perry_static_require_0.version + marker;")); + assert!(!got.contains(r#"require("../../../package.json")"#)); + } + #[test] fn hoists_allowed_package_literal_require() { let source = r#" diff --git a/crates/perry/tests/source_graph_export_regressions.rs b/crates/perry/tests/source_graph_export_regressions.rs index 58df217fed..ee0534a5e5 100644 --- a/crates/perry/tests/source_graph_export_regressions.rs +++ b/crates/perry/tests/source_graph_export_regressions.rs @@ -972,3 +972,5 @@ mod issue_10180; mod issue_10197; #[path = "source_graph_export_regressions/issue_10258.rs"] mod issue_10258; +#[path = "source_graph_export_regressions/issue_10758.rs"] +mod issue_10758; diff --git a/crates/perry/tests/source_graph_export_regressions/issue_10758.rs b/crates/perry/tests/source_graph_export_regressions/issue_10758.rs new file mode 100644 index 0000000000..f96d1054fa --- /dev/null +++ b/crates/perry/tests/source_graph_export_regressions/issue_10758.rs @@ -0,0 +1,30 @@ +use super::{compile_and_run, write}; + +#[test] +fn esm_module_resolves_static_json_require_from_its_own_directory() { + let dir = tempfile::tempdir().expect("tempdir"); + let package = dir.path().join("mongodb"); + let handshake = package.join("src/cmap/handshake"); + std::fs::create_dir_all(&handshake).expect("create package source tree"); + std::fs::write( + package.join("package.json"), + r#"{"name":"mongodb","version":"7.5.0"}"#, + ) + .expect("write package.json"); + std::fs::write(handshake.join("marker.ts"), "export const marker = '';\n") + .expect("write marker module"); + std::fs::write( + handshake.join("client_metadata.ts"), + "import { marker } from './marker';\n\ + export const driverVersion = require('../../../package.json').version + marker;\n", + ) + .expect("write client metadata module"); + write( + dir.path(), + "main.ts", + "import { driverVersion } from './mongodb/src/cmap/handshake/client_metadata';\n\ + console.log(driverVersion);\n", + ); + + assert_eq!(compile_and_run(dir.path(), "main.ts"), "7.5.0\n"); +}