Skip to content

Commit

Permalink
esm: provide named exports for builtin libs
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
devsnek committed May 11, 2018
1 parent 5096e24 commit f074612
Show file tree
Hide file tree
Showing 8 changed files with 282 additions and 16 deletions.
33 changes: 30 additions & 3 deletions doc/api/esm.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,16 +95,43 @@ When loaded via `import` these modules will provide a single `default` export
representing the value of `module.exports` at the time they finished evaluating.

```js
import fs from 'fs';
fs.readFile('./foo.txt', (err, body) => {
// foo.js
module.exports = { one: 1 };

// bar.js
import foo from './foo.js';
foo.one === 1; // true
```

Builtin modules will provide named exports of their public API, as well as a
default export which can be used for, among other things, modifying the named
exports. Named exports of builtin modules are updated when the corresponding
exports property is accessed, redefined, or deleted.

```js
import EventEmitter from 'events';
const e = new EventEmitter();
```

```js
import { readFile } from 'fs';
readFile('./foo.txt', (err, source) => {
if (err) {
console.error(err);
} else {
console.log(body);
console.log(source);
}
});
```

```js
import fs, { readFileSync } from 'fs';

fs.readFileSync = () => Buffer.from('Hello, ESM');

fs.readFileSync === readFileSync;
```

## Loader hooks

<!-- type=misc -->
Expand Down
84 changes: 81 additions & 3 deletions lib/internal/bootstrap/loaders.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,26 @@

(function bootstrapInternalLoaders(process, getBinding, getLinkedBinding,
getInternalBinding) {
const {
apply: ReflectApply,
deleteProperty: ReflectDeleteProperty,
get: ReflectGet,
getOwnPropertyDescriptor: ReflectGetOwnPropertyDescriptor,
has: ReflectHas,
set: ReflectSet,
} = Reflect;
const {
prototype: {
hasOwnProperty: ObjectHasOwnProperty,
},
create: ObjectCreate,
defineProperty: ObjectDefineProperty,
keys: ObjectKeys,
} = Object;

// Set up process.moduleLoadList
const moduleLoadList = [];
Object.defineProperty(process, 'moduleLoadList', {
ObjectDefineProperty(process, 'moduleLoadList', {
value: moduleLoadList,
configurable: true,
enumerable: true,
Expand All @@ -53,7 +69,7 @@

// Set up process.binding() and process._linkedBinding()
{
const bindingObj = Object.create(null);
const bindingObj = ObjectCreate(null);

process.binding = function binding(module) {
module = String(module);
Expand All @@ -77,7 +93,7 @@
// Set up internalBinding() in the closure
let internalBinding;
{
const bindingObj = Object.create(null);
const bindingObj = ObjectCreate(null);
internalBinding = function internalBinding(module) {
let mod = bindingObj[module];
if (typeof mod !== 'object') {
Expand All @@ -95,6 +111,8 @@
this.filename = `${id}.js`;
this.id = id;
this.exports = {};
this.reflect = undefined;
this.exportKeys = undefined;
this.loaded = false;
this.loading = false;
}
Expand Down Expand Up @@ -193,6 +211,12 @@
'\n});'
];

const getOwn = (target, property, receiver) => {
return ReflectApply(ObjectHasOwnProperty, target, [property]) ?
ReflectGet(target, property, receiver) :
undefined;
};

NativeModule.prototype.compile = function() {
let source = NativeModule.getSource(this.id);
source = NativeModule.wrap(source);
Expand All @@ -208,6 +232,60 @@
NativeModule.require;
fn(this.exports, requireFn, this, process);

if (config.experimentalModules && !NativeModule.isInternal(this.id)) {
this.exportKeys = ObjectKeys(this.exports);

const update = (property, value) => {
if (this.reflect !== undefined &&
ReflectApply(ObjectHasOwnProperty,
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.
ObjectDefineProperty(target, prop, descriptor);
if (typeof descriptor.get === 'function' &&
!ReflectHas(handler, 'get')) {
handler.get = (target, prop, receiver) => {
const value = ReflectGet(target, prop, receiver);
if (ReflectApply(ObjectHasOwnProperty, target, [prop]))
update(prop, value);
return value;
};
}
update(prop, getOwn(target, prop));
return true;
},
deleteProperty: (target, prop) => {
if (ReflectDeleteProperty(target, prop)) {
update(prop, undefined);
return true;
}
return false;
},
set: (target, prop, value, receiver) => {
const descriptor = ReflectGetOwnPropertyDescriptor(target, prop);
if (ReflectSet(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);
}

this.loaded = true;
} finally {
this.loading = false;
Expand Down
3 changes: 2 additions & 1 deletion lib/internal/modules/esm/create_dynamic_module.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,10 @@ const createDynamicModule = (exports, url = '', evaluate) => {
const module = new ModuleWrap(reexports, `${url}`);
module.link(async () => reflectiveModule);
module.instantiate();
reflect.namespace = module.namespace();
return {
module,
reflect
reflect,
};
};

Expand Down
17 changes: 12 additions & 5 deletions lib/internal/modules/esm/translators.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,18 @@ translators.set('cjs', async (url) => {
// through normal resolution
translators.set('builtin', async (url) => {
debug(`Translating BuiltinModule ${url}`);
return createDynamicModule(['default'], url, (reflect) => {
debug(`Loading BuiltinModule ${url}`);
const exports = NativeModule.require(url.slice(5));
reflect.exports.default.set(exports);
});
// slice 'node:' scheme
const id = url.slice(5);
NativeModule.require(id);
const module = NativeModule.getCached(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);
});
});

// Strategy for loading a node native module
Expand Down
1 change: 1 addition & 0 deletions test/es-module/test-esm-dynamic-import.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ function expectFsNamespace(result) {
Promise.resolve(result)
.then(common.mustCall(ns => {
assert.strictEqual(typeof ns.default.writeFile, 'function');
assert.strictEqual(typeof ns.writeFile, 'function');
}));
}

Expand Down
143 changes: 143 additions & 0 deletions test/es-module/test-esm-live-binding.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// Flags: --experimental-modules

import '../common';
import assert from 'assert';

import fs, { readFile, readFileSync } from 'fs';
import events, { defaultMaxListeners } from 'events';
import util from 'util';

const readFileDescriptor = Reflect.getOwnPropertyDescriptor(fs, 'readFile');
const readFileSyncDescriptor =
Reflect.getOwnPropertyDescriptor(fs, 'readFileSync');

const s = Symbol();
const fn = () => s;

Reflect.deleteProperty(fs, 'readFile');

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

fs.readFile = fn;

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

Reflect.defineProperty(fs, 'readFile', {
value: fn,
configurable: true,
writable: true,
});

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

Reflect.deleteProperty(fs, 'readFile');

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

let count = 0;

Reflect.defineProperty(fs, 'readFile', {
get() { return count; },
configurable: true,
});

count++;

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

let otherValue;

Reflect.defineProperty(fs, 'readFile', { // eslint-disable-line accessor-pairs
set(value) {
Reflect.deleteProperty(fs, 'readFile');
otherValue = value;
},
configurable: true,
});

Reflect.defineProperty(fs, 'readFileSync', {
get() {
return otherValue;
},
configurable: true,
});

fs.readFile = 2;

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

Reflect.defineProperty(fs, 'readFile', readFileDescriptor);
Reflect.defineProperty(fs, 'readFileSync', readFileSyncDescriptor);

const originDefaultMaxListeners = events.defaultMaxListeners;
const utilProto = util.__proto__; // eslint-disable-line no-proto

count = 0;

Reflect.defineProperty(Function.prototype, 'defaultMaxListeners', {
configurable: true,
enumerable: true,
get: function() { return ++count; },
set: function(v) {
Reflect.defineProperty(this, 'defaultMaxListeners', {
configurable: true,
enumerable: true,
writable: true,
value: v,
});
},
});

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

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

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

Function.prototype.defaultMaxListeners = 'foo';

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

count = 0;

const p = {
get foo() { return ++count; },
set foo(v) {
Reflect.defineProperty(this, 'foo', {
configurable: true,
enumerable: true,
writable: true,
value: v,
});
},
};

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

assert.strictEqual(util.foo, 1);

util.foo = 'bar';

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

p.foo = 'foo';

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

events.defaultMaxListeners = originDefaultMaxListeners;
util.__proto__ = utilProto; // eslint-disable-line no-proto

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

assert.throws(
() => Object.defineProperty(events, 'defaultMaxListeners', { value: 3 }),
/TypeError: Cannot redefine/
);
10 changes: 9 additions & 1 deletion test/es-module/test-esm-namespace.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,13 @@
import '../common';
import * as fs from 'fs';
import assert from 'assert';
import Module from 'module';

assert.deepStrictEqual(Object.keys(fs), ['default']);
const keys = Object.entries(
Object.getOwnPropertyDescriptors(new Module().require('fs')))
.filter(([name, d]) => d.enumerable)
.map(([name]) => name)
.concat('default')
.sort();

assert.deepStrictEqual(Object.keys(fs).sort(), keys);
Loading

0 comments on commit f074612

Please sign in to comment.