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: https://github.com/nodejs/node/pull/20403 Reviewed-By: Bradley Farias <bradley.meck@gmail.com> Reviewed-By: Guy Bedford <guybedford@gmail.com> Reviewed-By: Jan Krems <jan.krems@gmail.com>
This commit is contained in:
parent
5096e249e7
commit
f074612b74
@ -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.
|
representing the value of `module.exports` at the time they finished evaluating.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import fs from 'fs';
|
// foo.js
|
||||||
fs.readFile('./foo.txt', (err, body) => {
|
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) {
|
if (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
} else {
|
} else {
|
||||||
console.log(body);
|
console.log(source);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
import fs, { readFileSync } from 'fs';
|
||||||
|
|
||||||
|
fs.readFileSync = () => Buffer.from('Hello, ESM');
|
||||||
|
|
||||||
|
fs.readFileSync === readFileSync;
|
||||||
|
```
|
||||||
|
|
||||||
## Loader hooks
|
## Loader hooks
|
||||||
|
|
||||||
<!-- type=misc -->
|
<!-- type=misc -->
|
||||||
|
@ -41,10 +41,26 @@
|
|||||||
|
|
||||||
(function bootstrapInternalLoaders(process, getBinding, getLinkedBinding,
|
(function bootstrapInternalLoaders(process, getBinding, getLinkedBinding,
|
||||||
getInternalBinding) {
|
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
|
// Set up process.moduleLoadList
|
||||||
const moduleLoadList = [];
|
const moduleLoadList = [];
|
||||||
Object.defineProperty(process, 'moduleLoadList', {
|
ObjectDefineProperty(process, 'moduleLoadList', {
|
||||||
value: moduleLoadList,
|
value: moduleLoadList,
|
||||||
configurable: true,
|
configurable: true,
|
||||||
enumerable: true,
|
enumerable: true,
|
||||||
@ -53,7 +69,7 @@
|
|||||||
|
|
||||||
// Set up process.binding() and process._linkedBinding()
|
// Set up process.binding() and process._linkedBinding()
|
||||||
{
|
{
|
||||||
const bindingObj = Object.create(null);
|
const bindingObj = ObjectCreate(null);
|
||||||
|
|
||||||
process.binding = function binding(module) {
|
process.binding = function binding(module) {
|
||||||
module = String(module);
|
module = String(module);
|
||||||
@ -77,7 +93,7 @@
|
|||||||
// Set up internalBinding() in the closure
|
// Set up internalBinding() in the closure
|
||||||
let internalBinding;
|
let internalBinding;
|
||||||
{
|
{
|
||||||
const bindingObj = Object.create(null);
|
const bindingObj = ObjectCreate(null);
|
||||||
internalBinding = function internalBinding(module) {
|
internalBinding = function internalBinding(module) {
|
||||||
let mod = bindingObj[module];
|
let mod = bindingObj[module];
|
||||||
if (typeof mod !== 'object') {
|
if (typeof mod !== 'object') {
|
||||||
@ -95,6 +111,8 @@
|
|||||||
this.filename = `${id}.js`;
|
this.filename = `${id}.js`;
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.exports = {};
|
this.exports = {};
|
||||||
|
this.reflect = undefined;
|
||||||
|
this.exportKeys = undefined;
|
||||||
this.loaded = false;
|
this.loaded = false;
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
@ -193,6 +211,12 @@
|
|||||||
'\n});'
|
'\n});'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const getOwn = (target, property, receiver) => {
|
||||||
|
return ReflectApply(ObjectHasOwnProperty, target, [property]) ?
|
||||||
|
ReflectGet(target, property, receiver) :
|
||||||
|
undefined;
|
||||||
|
};
|
||||||
|
|
||||||
NativeModule.prototype.compile = function() {
|
NativeModule.prototype.compile = function() {
|
||||||
let source = NativeModule.getSource(this.id);
|
let source = NativeModule.getSource(this.id);
|
||||||
source = NativeModule.wrap(source);
|
source = NativeModule.wrap(source);
|
||||||
@ -208,6 +232,60 @@
|
|||||||
NativeModule.require;
|
NativeModule.require;
|
||||||
fn(this.exports, requireFn, this, process);
|
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;
|
this.loaded = true;
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
@ -52,9 +52,10 @@ const createDynamicModule = (exports, url = '', evaluate) => {
|
|||||||
const module = new ModuleWrap(reexports, `${url}`);
|
const module = new ModuleWrap(reexports, `${url}`);
|
||||||
module.link(async () => reflectiveModule);
|
module.link(async () => reflectiveModule);
|
||||||
module.instantiate();
|
module.instantiate();
|
||||||
|
reflect.namespace = module.namespace();
|
||||||
return {
|
return {
|
||||||
module,
|
module,
|
||||||
reflect
|
reflect,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -59,11 +59,18 @@ translators.set('cjs', async (url) => {
|
|||||||
// through normal resolution
|
// through normal resolution
|
||||||
translators.set('builtin', async (url) => {
|
translators.set('builtin', async (url) => {
|
||||||
debug(`Translating BuiltinModule ${url}`);
|
debug(`Translating BuiltinModule ${url}`);
|
||||||
return createDynamicModule(['default'], url, (reflect) => {
|
// slice 'node:' scheme
|
||||||
debug(`Loading BuiltinModule ${url}`);
|
const id = url.slice(5);
|
||||||
const exports = NativeModule.require(url.slice(5));
|
NativeModule.require(id);
|
||||||
reflect.exports.default.set(exports);
|
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
|
// Strategy for loading a node native module
|
||||||
|
@ -52,6 +52,7 @@ function expectFsNamespace(result) {
|
|||||||
Promise.resolve(result)
|
Promise.resolve(result)
|
||||||
.then(common.mustCall(ns => {
|
.then(common.mustCall(ns => {
|
||||||
assert.strictEqual(typeof ns.default.writeFile, 'function');
|
assert.strictEqual(typeof ns.default.writeFile, 'function');
|
||||||
|
assert.strictEqual(typeof ns.writeFile, 'function');
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
143
test/es-module/test-esm-live-binding.mjs
Normal file
143
test/es-module/test-esm-live-binding.mjs
Normal file
@ -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/
|
||||||
|
);
|
@ -2,5 +2,13 @@
|
|||||||
import '../common';
|
import '../common';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import assert from 'assert';
|
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);
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import _url from 'url';
|
import { URL } from 'url';
|
||||||
|
|
||||||
const builtins = new Set(
|
const builtins = new Set(
|
||||||
Object.keys(process.binding('natives')).filter(str =>
|
Object.keys(process.binding('natives')).filter(str =>
|
||||||
/^(?!(?:internal|node|v8)\/)/.test(str))
|
/^(?!(?:internal|node|v8)\/)/.test(str))
|
||||||
)
|
)
|
||||||
|
|
||||||
const baseURL = new _url.URL('file://');
|
const baseURL = new URL('file://');
|
||||||
baseURL.pathname = process.cwd() + '/';
|
baseURL.pathname = process.cwd() + '/';
|
||||||
|
|
||||||
export function resolve (specifier, base = baseURL) {
|
export function resolve (specifier, base = baseURL) {
|
||||||
@ -15,7 +16,7 @@ export function resolve (specifier, base = baseURL) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
// load all dependencies as esm, regardless of file extension
|
// 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 {
|
return {
|
||||||
url,
|
url,
|
||||||
format: 'esm'
|
format: 'esm'
|
||||||
|
Loading…
x
Reference in New Issue
Block a user