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
16 changes: 16 additions & 0 deletions ext/node/polyfills/01_require.js
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,22 @@ Module._load = function (request, parent, isMain) {
}
}

// Deno removes Object.prototype.__proto__ by default, so when CJS modules
// do `exports.__proto__ = someObj` to set up prototype chains, the assignment
// creates a regular data property instead of setting the prototype. Fix this
// by detecting the own __proto__ property and using Object.setPrototypeOf.
if (
module.exports &&
typeof module.exports === "object" &&
ObjectHasOwn(module.exports, "__proto__")
) {
const proto = module.exports["__proto__"];
if (proto === null || typeof proto === "object") {
delete module.exports["__proto__"];
ObjectSetPrototypeOf(module.exports, proto);
}
}

return module.exports;
};

Expand Down
15 changes: 14 additions & 1 deletion libs/node_resolver/analyze.rs
Original file line number Diff line number Diff line change
Expand Up @@ -727,7 +727,7 @@ if (import.meta.main) {
"#);

for (export_name, quoted_name) in &export_names_with_quoted {
if !matches!(*export_name, "default" | "module.exports") {
if !matches!(*export_name, "default" | "module.exports" | "__proto__") {
add_export(
builder,
export_name,
Expand Down Expand Up @@ -847,6 +847,19 @@ export { __deno_export_4__ as "module.exports" };
);
}

#[test]
fn test_exports_to_wrapper_module_proto_filtered() {
let url = Url::parse("file:///test/test.ts").unwrap();
let exports = BTreeSet::from(
["__proto__", "required"].map(|s| s.to_string()),
);
let text = exports_to_wrapper_module(&url, &exports);
// __proto__ should be filtered out and not appear as a named export
assert!(!text.contains("__proto__"));
assert!(text.contains("export const required = mod[\"required\"];"));
assert!(text.contains("export default mod;"));
}

#[test]
fn test_to_double_quote_string() {
assert_eq!(to_double_quote_string("test"), "\"test\"");
Expand Down
8 changes: 8 additions & 0 deletions tests/specs/run/cjs_proto_exports/__test__.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"tests": {
"cjs_proto_exports": {
"args": "run -R main.mjs",
"output": "main.out"
}
}
}
17 changes: 17 additions & 0 deletions tests/specs/run/cjs_proto_exports/base.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
module.exports = {
obj: {
isplain: function () {
return "isplain";
},
},
num: {
is: function () {
return "num.is";
},
},
str: {
is: function () {
return "str.is";
},
},
};
23 changes: 23 additions & 0 deletions tests/specs/run/cjs_proto_exports/main.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);

// Test that CJS modules using exports.__proto__ to set up prototype chains
// work correctly even when Object.prototype.__proto__ has been deleted.
const mod = require("./mod.js");

// Direct export should work
console.log("required:", typeof mod.required);

// Inherited exports via __proto__ should also work
console.log("obj:", JSON.stringify(mod.obj !== undefined));
console.log("obj.isplain:", typeof mod.obj.isplain);
console.log("num:", JSON.stringify(mod.num !== undefined));
console.log("num.is:", typeof mod.num.is);
console.log("str:", JSON.stringify(mod.str !== undefined));
console.log("str.is:", typeof mod.str.is);

// Default import should also work
const toi = await import("./mod.js");
console.log("default.required:", typeof toi.default.required);
console.log("default.obj:", JSON.stringify(toi.default.obj !== undefined));
console.log("default.obj.isplain:", typeof toi.default.obj.isplain);
10 changes: 10 additions & 0 deletions tests/specs/run/cjs_proto_exports/main.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
required: function
obj: true
obj.isplain: function
num: true
num.is: function
str: true
str.is: function
default.required: function
default.obj: true
default.obj.isplain: function
5 changes: 5 additions & 0 deletions tests/specs/run/cjs_proto_exports/mod.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
var base = require("./base");
exports.__proto__ = base;
exports.required = function () {
return "required";
};
3 changes: 3 additions & 0 deletions tests/specs/run/cjs_proto_exports/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"type": "commonjs"
}