logging importable exports shows an empty object
What versions & operating system are you using?
"@cloudflare/vite-plugin": "^1.15.3",
"@cloudflare/workers-types": "^4.20251127.0",
"typescript": "^5.9.3",
"vite": "^7.2.4",
"wrangler": "^4.51.0"
Please provide a link to a minimal reproduction
https://github.com/threepointone/repro-vite-exports-bug
Describe the Bug
importable exports aren't logged with the module's exports (as specified in https://github.com/cloudflare/workerd/pull/5516). ctx.exports are ok and work as expected.
Please provide any relevant error logs
No response
Looks like this is actually an issue when logging the exports object. Since it's a Proxy, it doesn't enumerate the members and logs an empty object. The properties are actually present.
Proxies can implement enumeration. RPC proxies don't because they actually don't know what methods the other side implements. But, the exports (and env) proxies ought to implement enumeration.
@anonrig
https://github.com/cloudflare/workerd/pull/5608 doesn't fix this issue. Importable exports are still logged as an empty object.
@jasnell any idea what's happening here?
I've opened #5647 to revert the changes from #5608 but keep the tests -- which still pass, because the change didn't actually do anything (the properties are already enumerable).
Testing the actual bug a little bit, I find:
- This applies to both importable env and exports. Presumably it's not new.
- It only seems to affect
console.log().JSON.stringify(), in contrast, sees all the members. This is very odd and suggests the problem is specific to console.log, not a general problem with the proxy? - toString() returns
[object Object], so it's not an issue with that.
I am not sure where console.log() behavior is implemented exactly, but I think this is something we have customized? Perhaps the bug is there?
In workerd, the console.log output ends up being routed through the nodejs_compat util.inspect and util.format methods, which does include some limited support for inspecting proxies. You'll find tho, that it doesn't really handle proxies all that well in the default case. Even in Node.js, the following ends up just printing {}:
const p = new Proxy({}, {
ownKeys() { return ['a']; },
getOwnPropertyDescriptor() {
return { value: 1, enumerable: true, configurable: true, writable: true };
},
has() { return true; },
get() { return 1; },
});
console.log(p); // In Node.js... prints '{}'
In Node.js, the util.format implements a %j flag (e.g. console.log('%j', p)) that causes it to start printing mostly correctly... only because it JSON serializes the object and prints the resulting string. We haven't implemented the %j flag so that doesn't work in workerd yet.
So the proxy itself is fine. What you're seeing here is a limitation of the Node.js util.format/util.inspect when interacting with proxies.
I've opened an issue in Node.js about this: https://github.com/nodejs/node/issues/60964
The correct fix here would be to update the implementation of util.inspect/util.format to handle proxies more effectively.
There's apparently a showProxy option for this:
https://github.com/cloudflare/workerd/blob/eb0bfa22376f6a20dd0dac08a077a022316e5c19/src/node/internal/internal_inspect.ts#L152-L156
Otherwise proxies are intentionally hidden:
https://github.com/cloudflare/workerd/blob/eb0bfa22376f6a20dd0dac08a077a022316e5c19/src/node/internal/internal_inspect.ts#L1128-L1139
It... really bugs me that there is a built-in function getProxyDetails() which permits unwrapping proxies. I realize it's in internal utils that (hopefully) can't be imported by application code, but there's a reason why there's no language built-in for this, and it bothers me to have a back door exposed to JS at all...
The showProxy option shows details about the proxy itself and not the proxied object. It turns out that the behavior here is intentional. The util.inspect/util/format logic is avoiding triggering proxy traps that potentially have side effects, which makes sense. Discussing within Node.js (https://github.com/nodejs/node/issues/60964) how we can iterate on this.
The getProxyDetails is a carry over from node.js for debugging and is there for node.js compat. It is not exposed to user code
Ok, in the interim.. a good solution here would be to have these Proxy objects implement the Node.js util.inspect.custom protocol. The Proxy objects themselves can have add an implementation of the custom inspect function that should be called by the implementation.
For instance,
const p = new Proxy({}, { ownKeys() { return ['a'] } });
console.log(p); // {}
p[util.inspect.custom] = () => 1;
console.log(p); // 1
Docs: https://nodejs.org/docs/latest/api/util.html#utilinspectcustom
Update: eh, actually, that only works for exports, not for env. Will have to think about it some more.
Update: Node.js will likely update it's handling here to output Proxy {} rather than just {} so at least it's a bit easier to understand the reasoning behing the empty output. https://github.com/nodejs/node/pull/61029