Skip to content

Commit 3aabe35

Browse files
committed
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: `import { readFile } from 'fs'` instead of importing the entire namespace, `import fs from 'fs'`, and calling `fs.readFile`. the default export is left as the entire namespace (module.exports)
1 parent b55a11d commit 3aabe35

File tree

9 files changed

+222
-13
lines changed

9 files changed

+222
-13
lines changed

doc/api/esm.md

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,26 @@ 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.
109+
110+
```js
111+
import EventEmitter from 'events';
112+
const e = new EventEmitter();
113+
```
114+
115+
```js
116+
import { readFile } from 'fs';
117+
readFile('./foo.txt', (err, body) => {
100118
if (err) {
101119
console.error(err);
102120
} else {
@@ -105,6 +123,14 @@ fs.readFile('./foo.txt', (err, body) => {
105123
});
106124
```
107125

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

110136
<!-- type=misc -->

lib/internal/bootstrap/loaders.js

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@
9595
this.filename = `${id}.js`;
9696
this.id = id;
9797
this.exports = {};
98+
this.reflect = undefined;
99+
this.exportKeys = undefined;
98100
this.loaded = false;
99101
this.loading = false;
100102
}
@@ -193,6 +195,40 @@
193195
'\n});'
194196
];
195197

198+
const { isProxy } = internalBinding('types');
199+
const {
200+
apply: ReflectApply,
201+
has: ReflectHas,
202+
get: ReflectGet,
203+
set: ReflectSet,
204+
defineProperty: ReflectDefineProperty,
205+
deleteProperty: ReflectDeleteProperty,
206+
getOwnPropertyDescriptor: ReflectGetOwnPropertyDescriptor,
207+
} = Reflect;
208+
const {
209+
toString: ObjectToString,
210+
} = Object.prototype;
211+
let isNative;
212+
{
213+
const { toString } = Function.prototype;
214+
const re = toString.call(toString)
215+
.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&')
216+
.replace(/toString|(function ).*?(?=\\\()/g, '$1.*?');
217+
const nativeRegExp = new RegExp(`^${re}$`);
218+
isNative = (fn) => {
219+
if (typeof fn === 'function') {
220+
try {
221+
if (nativeRegExp.test(toString.call(fn))) {
222+
const { name } = fn;
223+
if (typeof name !== 'string' || !/^bound /.test(name))
224+
return !isProxy(fn);
225+
}
226+
} catch (e) {}
227+
}
228+
return false;
229+
};
230+
}
231+
196232
NativeModule.prototype.compile = function() {
197233
let source = NativeModule.getSource(this.id);
198234
source = NativeModule.wrap(source);
@@ -208,6 +244,93 @@
208244
NativeModule.require;
209245
fn(this.exports, requireFn, this, process);
210246

247+
if (config.experimentalModules) {
248+
this.exportKeys = Object.keys(this.exports);
249+
250+
const update = (property, value) => {
251+
if (this.reflect !== undefined && this.exportKeys.includes(property))
252+
this.reflect.exports[property].set(value);
253+
};
254+
255+
const methodWrapMap = new WeakMap();
256+
257+
const wrap = (target, name, value) => {
258+
if (typeof value !== 'function' || !isNative(value)) {
259+
return value;
260+
}
261+
262+
if (methodWrapMap.has(value))
263+
return methodWrapMap.get(value);
264+
265+
const p = new Proxy(value, {
266+
apply: (t, thisArg, args) => {
267+
if (thisArg === proxy || (this.reflect !== undefined &&
268+
this.reflect.namespace !== undefined &&
269+
thisArg === this.reflect.namespace)) {
270+
thisArg = target;
271+
}
272+
return ReflectApply(t, thisArg, args);
273+
},
274+
__proto__: null,
275+
});
276+
277+
methodWrapMap.set(value, p);
278+
279+
return p;
280+
};
281+
282+
const proxy = new Proxy(this.exports, {
283+
set: (target, prop, value, receiver) => {
284+
if (receiver === proxy || (this.reflect !== undefined &&
285+
this.reflect.namespace !== undefined &&
286+
receiver === this.reflect.namespace))
287+
receiver = target;
288+
if (ReflectSet(target, prop, value, receiver)) {
289+
update(prop, ReflectGet(target, prop, receiver));
290+
return true;
291+
}
292+
return false;
293+
},
294+
defineProperty: (target, prop, descriptor) => {
295+
if (ReflectDefineProperty(target, prop, descriptor)) {
296+
update(prop, ReflectGet(target, prop));
297+
return true;
298+
}
299+
return false;
300+
},
301+
deleteProperty: (target, prop) => {
302+
if (ReflectDeleteProperty(target, prop)) {
303+
update(prop, undefined);
304+
return true;
305+
}
306+
return false;
307+
},
308+
getOwnPropertyDescriptor: (target, prop) => {
309+
const descriptor = ReflectGetOwnPropertyDescriptor(target, prop);
310+
if (descriptor && ReflectHas(descriptor, 'value'))
311+
descriptor.value = wrap(target, prop, descriptor.value);
312+
return descriptor;
313+
},
314+
get: (target, prop, receiver) => {
315+
if (receiver === proxy || (this.reflect !== undefined &&
316+
this.reflect.namespace !== undefined &&
317+
receiver === this.reflect.namespace)) {
318+
receiver = target;
319+
}
320+
const value = ReflectGet(target, prop, receiver);
321+
if (prop === Symbol.toStringTag &&
322+
typeof target !== 'function' &&
323+
typeof value !== 'string') {
324+
const toStringTag = ObjectToString.call(target).slice(8, -1);
325+
return toStringTag === 'Object' ? value : toStringTag;
326+
}
327+
return wrap(target, prop, value);
328+
},
329+
__proto__: null,
330+
});
331+
this.exports = proxy;
332+
}
333+
211334
this.loaded = true;
212335
} finally {
213336
this.loading = false;

lib/internal/bootstrap/node.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -403,7 +403,7 @@
403403
// If global console has the same method as inspector console,
404404
// then wrap these two methods into one. Native wrapper will preserve
405405
// the original stack.
406-
wrappedConsole[key] = consoleCall.bind(wrappedConsole,
406+
wrappedConsole[key] = consoleCall.bind(null,
407407
originalConsole[key],
408408
wrappedConsole[key],
409409
config);

lib/internal/modules/esm/create_dynamic_module.js

Lines 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

lib/internal/modules/esm/translators.js

Lines 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

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

Lines 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

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Flags: --experimental-modules
2+
3+
import '../common';
4+
import assert from 'assert';
5+
6+
import fs, { readFile } from 'fs';
7+
8+
const s = Symbol();
9+
const fn = () => s;
10+
11+
delete fs.readFile;
12+
assert.strictEqual(fs.readFile, undefined);
13+
assert.strictEqual(readFile, undefined);
14+
15+
fs.readFile = fn;
16+
17+
assert.strictEqual(fs.readFile(), s);
18+
assert.strictEqual(readFile(), s);
19+
20+
Reflect.deleteProperty(fs, 'readFile');
21+
22+
Reflect.defineProperty(fs, 'readFile', {
23+
value: fn,
24+
configurable: true,
25+
writable: true,
26+
});
27+
28+
assert.strictEqual(fs.readFile(), s);
29+
assert.strictEqual(readFile(), s);
30+
31+
Reflect.deleteProperty(fs, 'readFile');
32+
assert.strictEqual(fs.readFile, undefined);
33+
assert.strictEqual(readFile, undefined);
34+
35+
Reflect.defineProperty(fs, 'readFile', {
36+
get() { return fn; },
37+
set() {},
38+
configurable: true,
39+
});
40+
41+
assert.strictEqual(fs.readFile(), s);
42+
assert.strictEqual(readFile(), s);

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

Lines 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);

test/fixtures/es-module-loaders/js-loader.mjs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import _url from 'url';
1+
import { URL } from 'url';
2+
23
const builtins = new Set(
34
Object.keys(process.binding('natives')).filter(str =>
45
/^(?!(?:internal|node|v8)\/)/.test(str))
56
)
67

7-
const baseURL = new _url.URL('file://');
8+
const baseURL = new URL('file://');
89
baseURL.pathname = process.cwd() + '/';
910

1011
export function resolve (specifier, base = baseURL) {
@@ -15,7 +16,7 @@ export function resolve (specifier, base = baseURL) {
1516
};
1617
}
1718
// load all dependencies as esm, regardless of file extension
18-
const url = new _url.URL(specifier, base).href;
19+
const url = new URL(specifier, base).href;
1920
return {
2021
url,
2122
format: 'esm'

0 commit comments

Comments
 (0)