Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 104c3bc

Browse filesBrowse 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 <bradley.meck@gmail.com> Reviewed-By: Guy Bedford <guybedford@gmail.com> Reviewed-By: Jan Krems <jan.krems@gmail.com>
1 parent ebd102e commit 104c3bc
Copy full SHA for 104c3bc

File tree

Expand file treeCollapse file tree

8 files changed

+282
-16
lines changed
Open diff view settings
Filter options
Expand file treeCollapse file tree

8 files changed

+282
-16
lines changed
Open diff view settings
Collapse file

‎doc/api/esm.md‎

Copy file name to clipboardExpand all lines: doc/api/esm.md
+30-3Lines changed: 30 additions & 3 deletions
  • Display the source diff
  • Display the rich diff
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 -->
Collapse file

‎lib/internal/bootstrap/loaders.js‎

Copy file name to clipboardExpand all lines: lib/internal/bootstrap/loaders.js
+81-3Lines changed: 81 additions & 3 deletions
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;
Collapse file

‎lib/internal/modules/esm/create_dynamic_module.js‎

Copy file name to clipboardExpand all lines: lib/internal/modules/esm/create_dynamic_module.js
+2-1Lines changed: 2 additions & 1 deletion
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

Collapse file

‎lib/internal/modules/esm/translators.js‎

Copy file name to clipboardExpand all lines: lib/internal/modules/esm/translators.js
+12-5Lines changed: 12 additions & 5 deletions
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
Collapse file

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

Copy file name to clipboardExpand all lines: test/es-module/test-esm-dynamic-import.js
+1Lines changed: 1 addition & 0 deletions
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

Collapse file
+143Lines changed: 143 additions & 0 deletions
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+
);
Collapse file

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

Copy file name to clipboardExpand all lines: test/es-module/test-esm-namespace.mjs
+9-1Lines changed: 9 additions & 1 deletion
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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.