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: `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)
  • Loading branch information
devsnek committed Apr 29, 2018
1 parent b55a11d commit 3aabe35
Show file tree
Hide file tree
Showing 9 changed files with 222 additions and 13 deletions.
30 changes: 28 additions & 2 deletions doc/api/esm.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,26 @@ 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.

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

```js
import { readFile } from 'fs';
readFile('./foo.txt', (err, body) => {
if (err) {
console.error(err);
} else {
Expand All @@ -105,6 +123,14 @@ fs.readFile('./foo.txt', (err, body) => {
});
```

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

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

fs.readFileSync === readFileSync;
```

## Loader hooks

<!-- type=misc -->
Expand Down
123 changes: 123 additions & 0 deletions lib/internal/bootstrap/loaders.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,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 +195,40 @@
'\n});'
];

const { isProxy } = internalBinding('types');
const {
apply: ReflectApply,
has: ReflectHas,
get: ReflectGet,
set: ReflectSet,
defineProperty: ReflectDefineProperty,
deleteProperty: ReflectDeleteProperty,
getOwnPropertyDescriptor: ReflectGetOwnPropertyDescriptor,
} = Reflect;
const {
toString: ObjectToString,
} = Object.prototype;
let isNative;
{
const { toString } = Function.prototype;
const re = toString.call(toString)
.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&')
.replace(/toString|(function ).*?(?=\\\()/g, '$1.*?');
const nativeRegExp = new RegExp(`^${re}$`);
isNative = (fn) => {
if (typeof fn === 'function') {
try {
if (nativeRegExp.test(toString.call(fn))) {
const { name } = fn;
if (typeof name !== 'string' || !/^bound /.test(name))
return !isProxy(fn);
}
} catch (e) {}
}
return false;
};
}

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

if (config.experimentalModules) {
this.exportKeys = Object.keys(this.exports);

const update = (property, value) => {
if (this.reflect !== undefined && this.exportKeys.includes(property))
this.reflect.exports[property].set(value);
};

const methodWrapMap = new WeakMap();

const wrap = (target, name, value) => {
if (typeof value !== 'function' || !isNative(value)) {
return value;
}

if (methodWrapMap.has(value))
return methodWrapMap.get(value);

const p = new Proxy(value, {
apply: (t, thisArg, args) => {
if (thisArg === proxy || (this.reflect !== undefined &&
this.reflect.namespace !== undefined &&
thisArg === this.reflect.namespace)) {
thisArg = target;
}
return ReflectApply(t, thisArg, args);
},
__proto__: null,
});

methodWrapMap.set(value, p);

return p;
};

const proxy = new Proxy(this.exports, {
set: (target, prop, value, receiver) => {
if (receiver === proxy || (this.reflect !== undefined &&
this.reflect.namespace !== undefined &&
receiver === this.reflect.namespace))
receiver = target;
if (ReflectSet(target, prop, value, receiver)) {
update(prop, ReflectGet(target, prop, receiver));
return true;
}
return false;
},
defineProperty: (target, prop, descriptor) => {
if (ReflectDefineProperty(target, prop, descriptor)) {
update(prop, ReflectGet(target, prop));
return true;
}
return false;
},
deleteProperty: (target, prop) => {
if (ReflectDeleteProperty(target, prop)) {
update(prop, undefined);
return true;
}
return false;
},
getOwnPropertyDescriptor: (target, prop) => {
const descriptor = ReflectGetOwnPropertyDescriptor(target, prop);
if (descriptor && ReflectHas(descriptor, 'value'))
descriptor.value = wrap(target, prop, descriptor.value);
return descriptor;
},
get: (target, prop, receiver) => {
if (receiver === proxy || (this.reflect !== undefined &&
this.reflect.namespace !== undefined &&
receiver === this.reflect.namespace)) {
receiver = target;
}
const value = ReflectGet(target, prop, receiver);
if (prop === Symbol.toStringTag &&
typeof target !== 'function' &&
typeof value !== 'string') {
const toStringTag = ObjectToString.call(target).slice(8, -1);
return toStringTag === 'Object' ? value : toStringTag;
}
return wrap(target, prop, value);
},
__proto__: null,
});
this.exports = proxy;
}

this.loaded = true;
} finally {
this.loading = false;
Expand Down
2 changes: 1 addition & 1 deletion lib/internal/bootstrap/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,7 @@
// If global console has the same method as inspector console,
// then wrap these two methods into one. Native wrapper will preserve
// the original stack.
wrappedConsole[key] = consoleCall.bind(wrappedConsole,
wrappedConsole[key] = consoleCall.bind(null,
originalConsole[key],
wrappedConsole[key],
config);
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
42 changes: 42 additions & 0 deletions test/es-module/test-esm-live-binding.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Flags: --experimental-modules

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

import fs, { readFile } from 'fs';

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

delete fs.readFile;
assert.strictEqual(fs.readFile, undefined);
assert.strictEqual(readFile, undefined);

fs.readFile = fn;

assert.strictEqual(fs.readFile(), s);
assert.strictEqual(readFile(), s);

Reflect.deleteProperty(fs, 'readFile');

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

assert.strictEqual(fs.readFile(), s);
assert.strictEqual(readFile(), s);

Reflect.deleteProperty(fs, 'readFile');
assert.strictEqual(fs.readFile, undefined);
assert.strictEqual(readFile, undefined);

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

assert.strictEqual(fs.readFile(), s);
assert.strictEqual(readFile(), s);
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);
7 changes: 4 additions & 3 deletions test/fixtures/es-module-loaders/js-loader.mjs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import _url from 'url';
import { URL } from 'url';

const builtins = new Set(
Object.keys(process.binding('natives')).filter(str =>
/^(?!(?:internal|node|v8)\/)/.test(str))
)

const baseURL = new _url.URL('file://');
const baseURL = new URL('file://');
baseURL.pathname = process.cwd() + '/';

export function resolve (specifier, base = baseURL) {
Expand All @@ -15,7 +16,7 @@ export function resolve (specifier, base = baseURL) {
};
}
// load all dependencies as esm, regardless of file extension
const url = new _url.URL(specifier, base).href;
const url = new URL(specifier, base).href;
return {
url,
format: 'esm'
Expand Down

0 comments on commit 3aabe35

Please sign in to comment.