Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

esm: remove proxy for builtin exports #29737

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
136 changes: 84 additions & 52 deletions lib/internal/bootstrap/loaders.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ function NativeModule(id) {
this.id = id;
this.exports = {};
this.reflect = undefined;
this.esmFacade = undefined;
this.exportKeys = undefined;
this.loaded = false;
this.loading = false;
Expand Down Expand Up @@ -211,15 +212,19 @@ function requireWithFallbackInDeps(request) {
}

// This is exposed for public loaders
NativeModule.prototype.compileForPublicLoader = function(needToProxify) {
NativeModule.prototype.compileForPublicLoader = function(needToSyncExports) {
if (!this.canBeRequiredByUsers) {
// No code because this is an assertion against bugs
// eslint-disable-next-line no-restricted-syntax
throw new Error(`Should not compile ${this.id} for public use`);
}
this.compile();
if (needToProxify && !this.exportKeys) {
this.proxifyExports();
if (needToSyncExports) {
if (!this.exportKeys) {
this.exportKeys = Object.keys(this.exports);
}
this.getESMFacade();
this.syncExports();
}
return this.exports;
};
Expand All @@ -230,61 +235,88 @@ const getOwn = (target, property, receiver) => {
undefined;
};

NativeModule.prototype.getURL = function() {
return `node:${this.id}`;
};

NativeModule.prototype.getESMFacade = function() {
if (this.esmFacade) return this.esmFacade;
const createDynamicModule = nativeModuleRequire(
'internal/modules/esm/create_dynamic_module');
const url = this.getURL();
return this.esmFacade = createDynamicModule(
[], [...this.exportKeys, 'default'], url, (reflect) => {
this.reflect = reflect;
this.syncExports();
reflect.exports.default.set(this.exports);
});
};

// Provide named exports for all builtin libraries so that the libraries
// may be imported in a nicer way for ESM users. The default export is left
// as the entire namespace (module.exports) and wrapped in a proxy such
// that APMs and other behavior are still left intact.
NativeModule.prototype.proxifyExports = function() {
this.exportKeys = Object.keys(this.exports);

const update = (property, value) => {
if (this.reflect !== undefined &&
ObjectPrototype.hasOwnProperty(this.reflect.exports, property))
this.reflect.exports[property].set(value);
};

const handler = {
__proto__: null,
defineProperty: (target, prop, descriptor) => {
// Use `Object.defineProperty` instead of `Reflect.defineProperty`
// to throw the appropriate error if something goes wrong.
Object.defineProperty(target, prop, descriptor);
if (typeof descriptor.get === 'function' &&
!Reflect.has(handler, 'get')) {
handler.get = (target, prop, receiver) => {
const value = Reflect.get(target, prop, receiver);
if (ObjectPrototype.hasOwnProperty(target, prop))
update(prop, value);
return value;
};
}
update(prop, getOwn(target, prop));
return true;
},
deleteProperty: (target, prop) => {
if (Reflect.deleteProperty(target, prop)) {
update(prop, undefined);
return true;
}
return false;
},
set: (target, prop, value, receiver) => {
const descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
if (Reflect.set(target, prop, value, receiver)) {
if (descriptor && typeof descriptor.set === 'function') {
for (const key of this.exportKeys) {
update(key, getOwn(target, key, receiver));
}
} else {
update(prop, getOwn(target, prop, receiver));
}
return true;
}
return false;
NativeModule.prototype.syncExports = function() {

// const update = (property, value) => {
// if (this.reflect !== undefined &&
// ObjectPrototype.hasOwnProperty(this.reflect.exports, property))
// this.reflect.exports[property].set(value);
// };

const names = this.exportKeys;
if (this.reflect) {
for (let i = 0; i < names.length; i++) {
const exportName = names[i];
if (exportName === 'default') continue;
this.reflect.exports[exportName].set(
getOwn(this.exports, exportName, this.exports)
);
}
};
}

this.exports = new Proxy(this.exports, handler);
// const handler = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume this code will be removed before shipping?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, cleanup and docs are pending semantic reviews

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, I'll leave this thread as a reminder.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jkrems should be good for another review

// __proto__: null,
// defineProperty: (target, prop, descriptor) => {
// // Use `Object.defineProperty` instead of `Reflect.defineProperty`
// // to throw the appropriate error if something goes wrong.
// Object.defineProperty(target, prop, descriptor);
// if (typeof descriptor.get === 'function' &&
// !Reflect.has(handler, 'get')) {
// handler.get = (target, prop, receiver) => {
// const value = Reflect.get(target, prop, receiver);
// if (ObjectPrototype.hasOwnProperty(target, prop))
// update(prop, value);
// return value;
// };
// }
// update(prop, getOwn(target, prop));
// return true;
// },
// deleteProperty: (target, prop) => {
// if (Reflect.deleteProperty(target, prop)) {
// update(prop, undefined);
// return true;
// }
// return false;
// },
// set: (target, prop, value, receiver) => {
// const descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
// if (Reflect.set(target, prop, value, receiver)) {
// if (descriptor && typeof descriptor.set === 'function') {
// for (const key of this.exportKeys) {
// update(key, getOwn(target, key, receiver));
// }
// } else {
// update(prop, getOwn(target, prop, receiver));
// }
// return true;
// }
// return false;
// }
// };

// this.exports = new Proxy(this.exports, handler);
};

NativeModule.prototype.compile = function() {
Expand Down
8 changes: 8 additions & 0 deletions lib/internal/modules/cjs/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -1120,6 +1120,14 @@ Module._preloadModules = function(requests) {
parent.require(requests[n]);
};

Module.syncESMExports = function syncESMExports() {
guybedford marked this conversation as resolved.
Show resolved Hide resolved
for (const mod of NativeModule.map.values()) {
if (mod.canBeRequiredByUsers) {
mod.syncExports();
}
}
};

// Backwards compatibility
Module.Module = Module;

Expand Down
12 changes: 4 additions & 8 deletions lib/internal/modules/esm/translators.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,14 +128,10 @@ translators.set('builtin', async function builtinStrategy(url) {
if (!module) {
throw new ERR_UNKNOWN_BUILTIN_MODULE(id);
}
return createDynamicModule(
[], [...module.exportKeys, 'default'], url, (reflect) => {
debug(`Loading BuiltinModule ${url}`);
module.reflect = reflect;
for (const key of module.exportKeys)
reflect.exports[key].set(module.exports[key]);
reflect.exports.default.set(module.exports);
});
debug(`Loading BuiltinModule ${url}`);
const facade = module.getESMFacade();
module.syncExports();
guybedford marked this conversation as resolved.
Show resolved Hide resolved
return facade;
});

// Strategy for loading a JSON file
Expand Down
24 changes: 22 additions & 2 deletions test/es-module/test-esm-live-binding.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Flags: --experimental-modules
import '../common/index.mjs';
import assert from 'assert';
import { syncESMExports } from 'module';

import fs, { readFile, readFileSync } from 'fs';
import events, { defaultMaxListeners } from 'events';
Expand All @@ -14,10 +15,12 @@ const s = Symbol();
const fn = () => s;

Reflect.deleteProperty(fs, 'readFile');
syncESMExports();

assert.deepStrictEqual([fs.readFile, readFile], [undefined, undefined]);

fs.readFile = fn;
syncESMExports();

assert.deepStrictEqual([fs.readFile(), readFile()], [s, s]);

Expand All @@ -26,10 +29,12 @@ Reflect.defineProperty(fs, 'readFile', {
configurable: true,
writable: true,
});
syncESMExports();

assert.deepStrictEqual([fs.readFile(), readFile()], [s, s]);

Reflect.deleteProperty(fs, 'readFile');
syncESMExports();

assert.deepStrictEqual([fs.readFile, readFile], [undefined, undefined]);

Expand All @@ -39,10 +44,14 @@ Reflect.defineProperty(fs, 'readFile', {
get() { return count; },
configurable: true,
});
syncESMExports();

assert.deepStrictEqual([readFile, fs.readFile], [0, 0]);

count++;
syncESMExports();

assert.deepStrictEqual([readFile, fs.readFile, readFile], [0, 1, 1]);
assert.deepStrictEqual([fs.readFile, readFile], [1, 1]);

let otherValue;

Expand All @@ -62,6 +71,7 @@ Reflect.defineProperty(fs, 'readFileSync', {
});

fs.readFile = 2;
syncESMExports();

assert.deepStrictEqual([readFile, readFileSync], [undefined, 2]);

Expand All @@ -86,17 +96,23 @@ Reflect.defineProperty(Function.prototype, 'defaultMaxListeners', {
});
},
});
syncESMExports();

assert.strictEqual(defaultMaxListeners, originDefaultMaxListeners);
assert.strictEqual(events.defaultMaxListeners, originDefaultMaxListeners);

assert.strictEqual(++events.defaultMaxListeners,
events.defaultMaxListeners += 1;

assert.strictEqual(events.defaultMaxListeners,
originDefaultMaxListeners + 1);

syncESMExports();

assert.strictEqual(defaultMaxListeners, originDefaultMaxListeners + 1);
assert.strictEqual(Function.prototype.defaultMaxListeners, 1);

Function.prototype.defaultMaxListeners = 'foo';
syncESMExports();

assert.strictEqual(Function.prototype.defaultMaxListeners, 'foo');
assert.strictEqual(events.defaultMaxListeners, originDefaultMaxListeners + 1);
Expand All @@ -117,16 +133,19 @@ const p = {
};

util.__proto__ = p; // eslint-disable-line no-proto
syncESMExports();

assert.strictEqual(util.foo, 1);

util.foo = 'bar';
syncESMExports();

assert.strictEqual(count, 1);
assert.strictEqual(util.foo, 'bar');
assert.strictEqual(p.foo, 2);

p.foo = 'foo';
syncESMExports();

assert.strictEqual(p.foo, 'foo');

Expand All @@ -135,6 +154,7 @@ util.__proto__ = utilProto; // eslint-disable-line no-proto

Reflect.deleteProperty(util, 'foo');
Reflect.deleteProperty(Function.prototype, 'defaultMaxListeners');
syncESMExports();

assert.throws(
() => Object.defineProperty(events, 'defaultMaxListeners', { value: 3 }),
Expand Down