Skip to content

Commit 104c3bc

Browse files
devsnektargos
authored andcommitted
esm: provide named exports for builtin libs
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. PR-URL: #20403 Reviewed-By: Bradley Farias <[email protected]> Reviewed-By: Guy Bedford <[email protected]> Reviewed-By: Jan Krems <[email protected]>
1 parent ebd102e commit 104c3bc

File tree

8 files changed

+282
-16
lines changed

8 files changed

+282
-16
lines changed

doc/api/esm.md

+30-3
Original file line numberDiff line numberDiff line change
@@ -95,16 +95,43 @@ When loaded via `import` these modules will provide a single `default` export
9595
representing the value of `module.exports` at the time they finished evaluating.
9696

9797
```js
98-
import fs from 'fs';
99-
fs.readFile('./foo.txt', (err, body) => {
98+
// foo.js
99+
module.exports = { one: 1 };
100+
101+
// bar.js
102+
import foo from './foo.js';
103+
foo.one === 1; // true
104+
```
105+
106+
Builtin modules will provide named exports of their public API, as well as a
107+
default export which can be used for, among other things, modifying the named
108+
exports. Named exports of builtin modules are updated when the corresponding
109+
exports property is accessed, redefined, or deleted.
110+
111+
```js
112+
import EventEmitter from 'events';
113+
const e = new EventEmitter();
114+
```
115+
116+
```js
117+
import { readFile } from 'fs';
118+
readFile('./foo.txt', (err, source) => {
100119
if (err) {
101120
console.error(err);
102121
} else {
103-
console.log(body);
122+
console.log(source);
104123
}
105124
});
106125
```
107126

127+
```js
128+
import fs, { readFileSync } from 'fs';
129+
130+
fs.readFileSync = () => Buffer.from('Hello, ESM');
131+
132+
fs.readFileSync === readFileSync;
133+
```
134+
108135
## Loader hooks
109136

110137
<!-- type=misc -->

lib/internal/bootstrap/loaders.js

+81-3
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,26 @@
4141

4242
(function bootstrapInternalLoaders(process, getBinding, getLinkedBinding,
4343
getInternalBinding) {
44+
const {
45+
apply: ReflectApply,
46+
deleteProperty: ReflectDeleteProperty,
47+
get: ReflectGet,
48+
getOwnPropertyDescriptor: ReflectGetOwnPropertyDescriptor,
49+
has: ReflectHas,
50+
set: ReflectSet,
51+
} = Reflect;
52+
const {
53+
prototype: {
54+
hasOwnProperty: ObjectHasOwnProperty,
55+
},
56+
create: ObjectCreate,
57+
defineProperty: ObjectDefineProperty,
58+
keys: ObjectKeys,
59+
} = Object;
4460

4561
// Set up process.moduleLoadList
4662
const moduleLoadList = [];
47-
Object.defineProperty(process, 'moduleLoadList', {
63+
ObjectDefineProperty(process, 'moduleLoadList', {
4864
value: moduleLoadList,
4965
configurable: true,
5066
enumerable: true,
@@ -53,7 +69,7 @@
5369

5470
// Set up process.binding() and process._linkedBinding()
5571
{
56-
const bindingObj = Object.create(null);
72+
const bindingObj = ObjectCreate(null);
5773

5874
process.binding = function binding(module) {
5975
module = String(module);
@@ -77,7 +93,7 @@
7793
// Set up internalBinding() in the closure
7894
let internalBinding;
7995
{
80-
const bindingObj = Object.create(null);
96+
const bindingObj = ObjectCreate(null);
8197
internalBinding = function internalBinding(module) {
8298
let mod = bindingObj[module];
8399
if (typeof mod !== 'object') {
@@ -95,6 +111,8 @@
95111
this.filename = `${id}.js`;
96112
this.id = id;
97113
this.exports = {};
114+
this.reflect = undefined;
115+
this.exportKeys = undefined;
98116
this.loaded = false;
99117
this.loading = false;
100118
}
@@ -193,6 +211,12 @@
193211
'\n});'
194212
];
195213

214+
const getOwn = (target, property, receiver) => {
215+
return ReflectApply(ObjectHasOwnProperty, target, [property]) ?
216+
ReflectGet(target, property, receiver) :
217+
undefined;
218+
};
219+
196220
NativeModule.prototype.compile = function() {
197221
let source = NativeModule.getSource(this.id);
198222
source = NativeModule.wrap(source);
@@ -208,6 +232,60 @@
208232
NativeModule.require;
209233
fn(this.exports, requireFn, this, process);
210234

235+
if (config.experimentalModules && !NativeModule.isInternal(this.id)) {
236+
this.exportKeys = ObjectKeys(this.exports);
237+
238+
const update = (property, value) => {
239+
if (this.reflect !== undefined &&
240+
ReflectApply(ObjectHasOwnProperty,
241+
this.reflect.exports, [property]))
242+
this.reflect.exports[property].set(value);
243+
};
244+
245+
const handler = {
246+
__proto__: null,
247+
defineProperty: (target, prop, descriptor) => {
248+
// Use `Object.defineProperty` instead of `Reflect.defineProperty`
249+
// to throw the appropriate error if something goes wrong.
250+
ObjectDefineProperty(target, prop, descriptor);
251+
if (typeof descriptor.get === 'function' &&
252+
!ReflectHas(handler, 'get')) {
253+
handler.get = (target, prop, receiver) => {
254+
const value = ReflectGet(target, prop, receiver);
255+
if (ReflectApply(ObjectHasOwnProperty, target, [prop]))
256+
update(prop, value);
257+
return value;
258+
};
259+
}
260+
update(prop, getOwn(target, prop));
261+
return true;
262+
},
263+
deleteProperty: (target, prop) => {
264+
if (ReflectDeleteProperty(target, prop)) {
265+
update(prop, undefined);
266+
return true;
267+
}
268+
return false;
269+
},
270+
set: (target, prop, value, receiver) => {
271+
const descriptor = ReflectGetOwnPropertyDescriptor(target, prop);
272+
if (ReflectSet(target, prop, value, receiver)) {
273+
if (descriptor && typeof descriptor.set === 'function') {
274+
for (const key of this.exportKeys) {
275+
update(key, getOwn(target, key, receiver));
276+
}
277+
} else {
278+
update(prop, getOwn(target, prop, receiver));
279+
}
280+
return true;
281+
}
282+
return false;
283+
}
284+
};
285+
286+
this.exports = new Proxy(this.exports, handler);
287+
}
288+
211289
this.loaded = true;
212290
} finally {
213291
this.loading = false;

lib/internal/modules/esm/create_dynamic_module.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,10 @@ const createDynamicModule = (exports, url = '', evaluate) => {
5252
const module = new ModuleWrap(reexports, `${url}`);
5353
module.link(async () => reflectiveModule);
5454
module.instantiate();
55+
reflect.namespace = module.namespace();
5556
return {
5657
module,
57-
reflect
58+
reflect,
5859
};
5960
};
6061

lib/internal/modules/esm/translators.js

+12-5
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,18 @@ translators.set('cjs', async (url) => {
5959
// through normal resolution
6060
translators.set('builtin', async (url) => {
6161
debug(`Translating BuiltinModule ${url}`);
62-
return createDynamicModule(['default'], url, (reflect) => {
63-
debug(`Loading BuiltinModule ${url}`);
64-
const exports = NativeModule.require(url.slice(5));
65-
reflect.exports.default.set(exports);
66-
});
62+
// slice 'node:' scheme
63+
const id = url.slice(5);
64+
NativeModule.require(id);
65+
const module = NativeModule.getCached(id);
66+
return createDynamicModule(
67+
[...module.exportKeys, 'default'], url, (reflect) => {
68+
debug(`Loading BuiltinModule ${url}`);
69+
module.reflect = reflect;
70+
for (const key of module.exportKeys)
71+
reflect.exports[key].set(module.exports[key]);
72+
reflect.exports.default.set(module.exports);
73+
});
6774
});
6875

6976
// Strategy for loading a node native module

test/es-module/test-esm-dynamic-import.js

+1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ function expectFsNamespace(result) {
5252
Promise.resolve(result)
5353
.then(common.mustCall(ns => {
5454
assert.strictEqual(typeof ns.default.writeFile, 'function');
55+
assert.strictEqual(typeof ns.writeFile, 'function');
5556
}));
5657
}
5758

+143
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
// Flags: --experimental-modules
2+
3+
import '../common';
4+
import assert from 'assert';
5+
6+
import fs, { readFile, readFileSync } from 'fs';
7+
import events, { defaultMaxListeners } from 'events';
8+
import util from 'util';
9+
10+
const readFileDescriptor = Reflect.getOwnPropertyDescriptor(fs, 'readFile');
11+
const readFileSyncDescriptor =
12+
Reflect.getOwnPropertyDescriptor(fs, 'readFileSync');
13+
14+
const s = Symbol();
15+
const fn = () => s;
16+
17+
Reflect.deleteProperty(fs, 'readFile');
18+
19+
assert.deepStrictEqual([fs.readFile, readFile], [undefined, undefined]);
20+
21+
fs.readFile = fn;
22+
23+
assert.deepStrictEqual([fs.readFile(), readFile()], [s, s]);
24+
25+
Reflect.defineProperty(fs, 'readFile', {
26+
value: fn,
27+
configurable: true,
28+
writable: true,
29+
});
30+
31+
assert.deepStrictEqual([fs.readFile(), readFile()], [s, s]);
32+
33+
Reflect.deleteProperty(fs, 'readFile');
34+
35+
assert.deepStrictEqual([fs.readFile, readFile], [undefined, undefined]);
36+
37+
let count = 0;
38+
39+
Reflect.defineProperty(fs, 'readFile', {
40+
get() { return count; },
41+
configurable: true,
42+
});
43+
44+
count++;
45+
46+
assert.deepStrictEqual([readFile, fs.readFile, readFile], [0, 1, 1]);
47+
48+
let otherValue;
49+
50+
Reflect.defineProperty(fs, 'readFile', { // eslint-disable-line accessor-pairs
51+
set(value) {
52+
Reflect.deleteProperty(fs, 'readFile');
53+
otherValue = value;
54+
},
55+
configurable: true,
56+
});
57+
58+
Reflect.defineProperty(fs, 'readFileSync', {
59+
get() {
60+
return otherValue;
61+
},
62+
configurable: true,
63+
});
64+
65+
fs.readFile = 2;
66+
67+
assert.deepStrictEqual([readFile, readFileSync], [undefined, 2]);
68+
69+
Reflect.defineProperty(fs, 'readFile', readFileDescriptor);
70+
Reflect.defineProperty(fs, 'readFileSync', readFileSyncDescriptor);
71+
72+
const originDefaultMaxListeners = events.defaultMaxListeners;
73+
const utilProto = util.__proto__; // eslint-disable-line no-proto
74+
75+
count = 0;
76+
77+
Reflect.defineProperty(Function.prototype, 'defaultMaxListeners', {
78+
configurable: true,
79+
enumerable: true,
80+
get: function() { return ++count; },
81+
set: function(v) {
82+
Reflect.defineProperty(this, 'defaultMaxListeners', {
83+
configurable: true,
84+
enumerable: true,
85+
writable: true,
86+
value: v,
87+
});
88+
},
89+
});
90+
91+
assert.strictEqual(defaultMaxListeners, originDefaultMaxListeners);
92+
assert.strictEqual(events.defaultMaxListeners, originDefaultMaxListeners);
93+
94+
assert.strictEqual(++events.defaultMaxListeners,
95+
originDefaultMaxListeners + 1);
96+
97+
assert.strictEqual(defaultMaxListeners, originDefaultMaxListeners + 1);
98+
assert.strictEqual(Function.prototype.defaultMaxListeners, 1);
99+
100+
Function.prototype.defaultMaxListeners = 'foo';
101+
102+
assert.strictEqual(Function.prototype.defaultMaxListeners, 'foo');
103+
assert.strictEqual(events.defaultMaxListeners, originDefaultMaxListeners + 1);
104+
assert.strictEqual(defaultMaxListeners, originDefaultMaxListeners + 1);
105+
106+
count = 0;
107+
108+
const p = {
109+
get foo() { return ++count; },
110+
set foo(v) {
111+
Reflect.defineProperty(this, 'foo', {
112+
configurable: true,
113+
enumerable: true,
114+
writable: true,
115+
value: v,
116+
});
117+
},
118+
};
119+
120+
util.__proto__ = p; // eslint-disable-line no-proto
121+
122+
assert.strictEqual(util.foo, 1);
123+
124+
util.foo = 'bar';
125+
126+
assert.strictEqual(count, 1);
127+
assert.strictEqual(util.foo, 'bar');
128+
assert.strictEqual(p.foo, 2);
129+
130+
p.foo = 'foo';
131+
132+
assert.strictEqual(p.foo, 'foo');
133+
134+
events.defaultMaxListeners = originDefaultMaxListeners;
135+
util.__proto__ = utilProto; // eslint-disable-line no-proto
136+
137+
Reflect.deleteProperty(util, 'foo');
138+
Reflect.deleteProperty(Function.prototype, 'defaultMaxListeners');
139+
140+
assert.throws(
141+
() => Object.defineProperty(events, 'defaultMaxListeners', { value: 3 }),
142+
/TypeError: Cannot redefine/
143+
);

test/es-module/test-esm-namespace.mjs

+9-1
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,13 @@
22
import '../common';
33
import * as fs from 'fs';
44
import assert from 'assert';
5+
import Module from 'module';
56

6-
assert.deepStrictEqual(Object.keys(fs), ['default']);
7+
const keys = Object.entries(
8+
Object.getOwnPropertyDescriptors(new Module().require('fs')))
9+
.filter(([name, d]) => d.enumerable)
10+
.map(([name]) => name)
11+
.concat('default')
12+
.sort();
13+
14+
assert.deepStrictEqual(Object.keys(fs).sort(), keys);

0 commit comments

Comments
 (0)