diff --git a/changelog.d/9885-perf-hooks-default-export-keys.md b/changelog.d/9885-perf-hooks-default-export-keys.md new file mode 100644 index 0000000000..04d1e4a361 --- /dev/null +++ b/changelog.d/9885-perf-hooks-default-export-keys.md @@ -0,0 +1,3 @@ +### Fixed + +- Match Node's `node:perf_hooks` default-import keys while preserving namespace `default` and shared export identity. diff --git a/crates/perry-dispatch/src/cjs_default_modules.rs b/crates/perry-dispatch/src/cjs_default_modules.rs index 798f436d19..d1a05c5e33 100644 --- a/crates/perry-dispatch/src/cjs_default_modules.rs +++ b/crates/perry-dispatch/src/cjs_default_modules.rs @@ -47,6 +47,7 @@ cjs_default_namespace_modules!( "path", "path.posix", "path.win32", + "perf_hooks", "process", "punycode", "querystring", diff --git a/crates/perry-hir/src/lower/tests.rs b/crates/perry-hir/src/lower/tests.rs index d13fbea68b..fcf1c2943d 100644 --- a/crates/perry-hir/src/lower/tests.rs +++ b/crates/perry-hir/src/lower/tests.rs @@ -582,6 +582,39 @@ export function perfHooksDefault() { )); } +#[test] +fn native_perf_hooks_default_import_reads_cjs_namespace() { + let source = r#" +import hooks from "node:perf_hooks"; +export function perfHooksDefaultImport() { + return hooks; +} +"#; + let module = perry_parser::parse_typescript(source, "perf-hooks-default-import.ts") + .expect("source parses"); + let hir = super::lower_module( + &module, + "perf-hooks-default-import", + "perf-hooks-default-import.ts", + ) + .expect("source lowers"); + let function = hir + .functions + .iter() + .find(|function| function.name == "perfHooksDefaultImport") + .expect("exported function is lowered"); + + assert!(matches!( + function.body.as_slice(), + [Stmt::Return(Some(crate::ir::Expr::PropertyGet { + object, + property, + .. + }))] if property == "default" + && matches!(object.as_ref(), crate::ir::Expr::NativeModuleRef(module) if module == "perf_hooks") + )); +} + #[test] fn test_lower_type_param_scoping() { let mut ctx = make_ctx(); diff --git a/crates/perry-runtime/src/object/native_module.rs b/crates/perry-runtime/src/object/native_module.rs index c27e5d5ffe..08e282c625 100644 --- a/crates/perry-runtime/src/object/native_module.rs +++ b/crates/perry-runtime/src/object/native_module.rs @@ -702,14 +702,6 @@ pub(crate) fn cjs_default_export_value(module_name: &str) -> Option { "dgram".len(), )), "module" => Some(bound_native_callable_export_value("module", "Module")), - // node:perf_hooks has no distinct CJS shape — `module.exports` IS the - // namespace, and `default` is listed among its keys. Resolving to the - // same tag keeps `hooks.default.performance === hooks.performance` - // (the `performance` singleton resolves identically from either). - "perf_hooks" => Some(js_create_native_module_namespace( - b"perf_hooks".as_ptr(), - "perf_hooks".len(), - )), "process" => Some(js_create_native_module_namespace( b"process".as_ptr(), "process".len(), @@ -812,6 +804,7 @@ fn should_cache_native_module_namespace(module_name: &str) -> bool { | "path.default" | "path.posix.default" | "path.win32.default" + | "perf_hooks.default" | "punycode" | "punycode.default" | "punycode.ucs2" @@ -936,12 +929,13 @@ unsafe fn native_module_property_by_name_impl( // `typeof performance === "object"`, `performance.timeOrigin` (a // constant), `performance.now` (a callable export), and // `constants.NODE_PERFORMANCE_GC_*` (constants) all dispatch coherently. - if module_name == "perf_hooks" && property_name == "performance" { + if matches!(module_name, "perf_hooks" | "perf_hooks.default") && property_name == "performance" + { // Singleton so `require("perf_hooks").performance` and the global // `performance` are the same object (Node identity guarantee, #1327). return crate::perf_hooks::performance_namespace(); } - if module_name == "perf_hooks" && property_name == "constants" { + if matches!(module_name, "perf_hooks" | "perf_hooks.default") && property_name == "constants" { // Its OWN tag. Sharing the `perf_hooks` tag made every read of the // constants object resolve against the MODULE's surface, so // `Object.keys(constants)` enumerated the export list instead of the diff --git a/crates/perry-runtime/src/object/native_module/module_keys.rs b/crates/perry-runtime/src/object/native_module/module_keys.rs index bb6a4c914e..09efe3f2bd 100644 --- a/crates/perry-runtime/src/object/native_module/module_keys.rs +++ b/crates/perry-runtime/src/object/native_module/module_keys.rs @@ -1281,6 +1281,22 @@ const VM_MODULE_NAMESPACE_KEYS: &[&[u8]] = &[ const VM_CONSTANTS_KEYS: &[&[u8]] = &[b"USE_MAIN_CONTEXT_DEFAULT_LOADER", b"DONT_CONTEXTIFY"]; +const PERF_HOOKS_DEFAULT_KEYS: &[&[u8]] = &[ + b"Performance", + b"PerformanceEntry", + b"PerformanceMark", + b"PerformanceMeasure", + b"PerformanceObserver", + b"PerformanceObserverEntryList", + b"PerformanceResourceTiming", + b"monitorEventLoopDelay", + b"eventLoopUtilization", + b"timerify", + b"createHistogram", + b"performance", + b"constants", +]; + // Linux-only open() flags: Node only enumerates these on platforms whose libc // defines them (e.g. `O_DIRECT`/`O_NOATIME` are absent on macOS), so gate the // enumerable-key tail by target so `Object.keys(constants)` matches Node here. @@ -1862,6 +1878,7 @@ pub(crate) fn native_module_enumerable_keys(module_name: &str) -> Option<&'stati b"constants", b"default", ]), + "perf_hooks.default" => Some(PERF_HOOKS_DEFAULT_KEYS), "perf_hooks.constants" => Some(&[ b"NODE_PERFORMANCE_GC_MAJOR", b"NODE_PERFORMANCE_GC_MINOR", diff --git a/crates/perry-runtime/src/object/native_module_dispatch.rs b/crates/perry-runtime/src/object/native_module_dispatch.rs index ded6e1039e..ff17dfdd2b 100644 --- a/crates/perry-runtime/src/object/native_module_dispatch.rs +++ b/crates/perry-runtime/src/object/native_module_dispatch.rs @@ -390,6 +390,7 @@ mod cjs_default_dispatch_tests { "path.default", "path.posix.default", "path.win32.default", + "perf_hooks.default", "process.default", "punycode.default", "querystring.default",