Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions changelog.d/10758-esm-static-json-require.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion crates/perry/src/commands/compile/collect_modules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@ fn transform_static_literal_requires(
compile_packages: &HashSet<String>,
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(
source: &str,
compile_packages: &HashSet<String>,
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 =
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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#"
Expand Down
2 changes: 2 additions & 0 deletions crates/perry/tests/source_graph_export_regressions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
30 changes: 30 additions & 0 deletions crates/perry/tests/source_graph_export_regressions/issue_10758.rs
Original file line number Diff line number Diff line change
@@ -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");
}
Loading