diff --git a/changelog.d/8877-release-r23-node-function-prototype.md b/changelog.d/8877-release-r23-node-function-prototype.md new file mode 100644 index 0000000000..7ef8ca2fe4 --- /dev/null +++ b/changelog.d/8877-release-r23-node-function-prototype.md @@ -0,0 +1,3 @@ +### Fixed + +- Match Node's prototype surface for callback-style native module exports such as `fs.readFile`. diff --git a/crates/perry-runtime/src/object/native_module/constructor_exports.rs b/crates/perry-runtime/src/object/native_module/constructor_exports.rs index 14fdd8066f..402cd26e41 100644 --- a/crates/perry-runtime/src/object/native_module/constructor_exports.rs +++ b/crates/perry-runtime/src/object/native_module/constructor_exports.rs @@ -64,10 +64,6 @@ pub(crate) fn is_native_module_constructor_export(module: &str, property: &str) "crypto.KeyObject" => property == "from", "dns" | "dns/promises" => property == "getServers", "events" => property == "once", - // Node's callback-style fs helpers are callable but not constructors. - // Keep this exact: class exports such as ReadStream/WriteStream must - // still materialize a real prototype for userland subclassing. - "fs" => property == "readFile", "http" => property == "setMaxIdleHTTPParsers", "inspector" => matches!(property, "close" | "url"), "inspector.DOMStorage" => matches!( @@ -199,8 +195,8 @@ mod tests { use super::is_native_module_constructor_export; #[test] - fn fs_read_file_is_not_a_constructor_but_stream_classes_are() { - assert!(!is_native_module_constructor_export("fs", "readFile")); + fn fs_read_file_and_stream_classes_expose_prototypes() { + assert!(is_native_module_constructor_export("fs", "readFile")); assert!(is_native_module_constructor_export("fs", "ReadStream")); assert!(is_native_module_constructor_export( "node:fs", diff --git a/crates/perry/tests/issue_5268_native_ctor_prototype_undefined.rs b/crates/perry/tests/issue_5268_native_ctor_prototype_undefined.rs index 6a0d58a8ba..70f52d796c 100644 --- a/crates/perry/tests/issue_5268_native_ctor_prototype_undefined.rs +++ b/crates/perry/tests/issue_5268_native_ctor_prototype_undefined.rs @@ -24,9 +24,9 @@ //! `Object.create(undefined)` / `Object.setPrototypeOf(x, undefined)` then hit //! the spec TypeError. Fix: recognize constructor-cased bound-native exports //! (leading uppercase, not flagged non-constructable) and let the -//! synthetic-class path materialize a stable `.prototype` object — while -//! keeping non-constructor exports (`fs.readFile`, …) at `prototype === -//! undefined`, matching Node's built-in non-constructor functions. +//! synthetic-class path materialize a stable `.prototype` object. Node also +//! exposes a real prototype on callback-style JavaScript wrapper exports such +//! as `fs.readFile`, despite the lower-case export name. use std::path::PathBuf; use std::process::Command; @@ -97,13 +97,13 @@ const prototype: any = { child() { return null; } }; Object.setPrototypeOf(prototype, (EventEmitter as any).prototype); console.log("setproto:", true); -// Non-constructor native exports keep prototype === undefined (no spurious -// synthesis), matching Node's built-in non-constructor functions. -console.log("nonctor:", (fs as any).readFile.prototype === undefined); +// Node's callback-style JavaScript wrapper exports also expose a prototype. +const readFileProto = (fs as any).readFile.prototype; +console.log("readFile:", typeof readFileProto === "object" && readFileProto !== null); "#, ); assert_eq!( stdout, - "rs: true\nws: true\nee: true\ncreate: true\nchain: true\nsetproto: true\nnonctor: true\n" + "rs: true\nws: true\nee: true\ncreate: true\nchain: true\nsetproto: true\nreadFile: true\n" ); } diff --git a/test-files/test_issue_5268_native_ctor_prototype_undefined.ts b/test-files/test_issue_5268_native_ctor_prototype_undefined.ts index 0b7e1d89f2..58a8440850 100644 --- a/test-files/test_issue_5268_native_ctor_prototype_undefined.ts +++ b/test-files/test_issue_5268_native_ctor_prototype_undefined.ts @@ -42,6 +42,10 @@ const prototype: any = { child() { return null; } }; Object.setPrototypeOf(prototype, (EventEmitter as any).prototype); console.log("setPrototypeOf(obj, EventEmitter.prototype) ok:", true); -// 4. Non-constructor native exports keep `prototype === undefined`, matching -// Node's built-in non-constructor functions (no spurious synthesis). -console.log("fs.readFile.prototype is undefined:", (fs as any).readFile.prototype === undefined); +// 4. Node also exposes a real prototype on callback-style JavaScript wrapper +// exports such as fs.readFile, despite the lower-case export name. +const readFileProto = (fs as any).readFile.prototype; +console.log( + "fs.readFile.prototype is object:", + typeof readFileProto === "object" && readFileProto !== null, +);