Skip to content

Commit d596353

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 d596353

File tree

9 files changed

+221
-14
lines changed

9 files changed

+221
-14
lines changed

doc/api/esm.md

+29-3
Original file line numberDiff line numberDiff line change
@@ -95,16 +95,42 @@ 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, source) => {
100118
if (err) {
101119
console.error(err);
102120
} else {
103-
console.log(body);
121+
console.log(source);
104122
}
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

+121
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,38 @@
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+
hasOwnProperty: ObjectHasOwnProperty,
211+
} = Object.prototype;
212+
let isNative;
213+
{
214+
const { toString } = Function.prototype;
215+
const re = toString.call(toString)
216+
.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&')
217+
.replace(/toString|(function ).*?(?=\\\()/g, '$1.*?');
218+
const nativeRegExp = new RegExp(`^${re}$`);
219+
isNative = (fn) => {
220+
if (typeof fn === 'function' &&
221+
nativeRegExp.test(toString.call(fn))) {
222+
const { name } = fn;
223+
if (typeof name !== 'string' || !/^bound /.test(name))
224+
return !isProxy(fn);
225+
}
226+
return false;
227+
};
228+
}
229+
196230
NativeModule.prototype.compile = function() {
197231
let source = NativeModule.getSource(this.id);
198232
source = NativeModule.wrap(source);
@@ -208,6 +242,93 @@
208242
NativeModule.require;
209243
fn(this.exports, requireFn, this, process);
210244

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

lib/internal/bootstrap/node.js

+1-1
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

+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

+42
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

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

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

+4-3
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)