From 84c9e4f1b51e06835816b27d2fab51844a95fc7a Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Mon, 9 Dec 2019 00:28:59 +0800 Subject: [PATCH] lib: refactor NativeModule Refactor the internal NativeModule class to a JS class and add more documentation about its properties. PR-URL: https://github.com/nodejs/node/pull/30856 Reviewed-By: Denys Otrishko Reviewed-By: Gus Caplan Reviewed-By: Rich Trott --- lib/internal/bootstrap/loaders.js | 280 ++++++++++++++++-------------- 1 file changed, 147 insertions(+), 133 deletions(-) diff --git a/lib/internal/bootstrap/loaders.js b/lib/internal/bootstrap/loaders.js index 1a0db5f882fc6f..5422f1335dec33 100644 --- a/lib/internal/bootstrap/loaders.js +++ b/lib/internal/bootstrap/loaders.js @@ -140,168 +140,182 @@ let internalBinding; }; } -// Think of this as module.exports in this file even though it is not -// written in CommonJS style. -const loaderExports = { - internalBinding, - NativeModule, - require: nativeModuleRequire -}; - const loaderId = 'internal/bootstrap/loaders'; - -// Set up NativeModule. -function NativeModule(id) { - this.filename = `${id}.js`; - this.id = id; - this.exports = {}; - this.module = undefined; - this.exportKeys = undefined; - this.loaded = false; - this.loading = false; - this.canBeRequiredByUsers = !id.startsWith('internal/'); -} - -// To be called during pre-execution when --expose-internals is on. -// Enables the user-land module loader to access internal modules. -NativeModule.exposeInternals = function() { - for (const [id, mod] of NativeModule.map) { - // Do not expose this to user land even with --expose-internals. - if (id !== loaderId) { - mod.canBeRequiredByUsers = true; - } - } -}; - const { moduleIds, compileFunction } = internalBinding('native_module'); -NativeModule.map = new Map(); -for (let i = 0; i < moduleIds.length; ++i) { - const id = moduleIds[i]; - const mod = new NativeModule(id); - NativeModule.map.set(id, mod); -} +const getOwn = (target, property, receiver) => { + return ObjectPrototypeHasOwnProperty(target, property) ? + ReflectGet(target, property, receiver) : + undefined; +}; -function nativeModuleRequire(id) { - if (id === loaderId) { - return loaderExports; +/** + * An internal abstraction for the built-in JavaScript modules of Node.js. + * Be careful not to expose this to user land unless --expose-internals is + * used, in which case there is no compatibility guarantee about this class. + */ +class NativeModule { + /** + * A map from the module IDs to the module instances. + * @type {Map} + */ + static map = new Map(moduleIds.map((id) => [id, new NativeModule(id)])); + + constructor(id) { + this.filename = `${id}.js`; + this.id = id; + this.canBeRequiredByUsers = !id.startsWith('internal/'); + + // The CJS exports object of the module. + this.exports = {}; + // States used to work around circular dependencies. + this.loaded = false; + this.loading = false; + + // The following properties are used by the ESM implementation and only + // initialized when the native module is loaded by users. + /** + * The C++ ModuleWrap binding used to interface with the ESM implementation. + * @type {ModuleWrap|undefined} + */ + this.module = undefined; + /** + * Exported names for the ESM imports. + * @type {string[]|undefined} + */ + this.exportKeys = undefined; } - const mod = NativeModule.map.get(id); - // Can't load the internal errors module from here, have to use a raw error. - // eslint-disable-next-line no-restricted-syntax - if (!mod) throw new TypeError(`Missing internal module '${id}'`); - return mod.compile(); -} + // To be called during pre-execution when --expose-internals is on. + // Enables the user-land module loader to access internal modules. + static exposeInternals() { + for (const [id, mod] of NativeModule.map) { + // Do not expose this to user land even with --expose-internals. + if (id !== loaderId) { + mod.canBeRequiredByUsers = true; + } + } + } -NativeModule.exists = function(id) { - return NativeModule.map.has(id); -}; + static exists(id) { + return NativeModule.map.has(id); + } -NativeModule.canBeRequiredByUsers = function(id) { - const mod = NativeModule.map.get(id); - return mod && mod.canBeRequiredByUsers; -}; + static canBeRequiredByUsers(id) { + const mod = NativeModule.map.get(id); + return mod && mod.canBeRequiredByUsers; + } -// Allow internal modules from dependencies to require -// other modules from dependencies by providing fallbacks. -function requireWithFallbackInDeps(request) { - if (!NativeModule.map.has(request)) { - request = `internal/deps/${request}`; + // Used by user-land module loaders to compile and load builtins. + compileForPublicLoader(needToSyncExports) { + if (!this.canBeRequiredByUsers) { + // No code because this is an assertion against bugs + // eslint-disable-next-line no-restricted-syntax + throw new Error(`Should not compile ${this.id} for public use`); + } + this.compileForInternalLoader(); + if (needToSyncExports) { + if (!this.exportKeys) { + // When using --expose-internals, we do not want to reflect the named + // exports from core modules as this can trigger unnecessary getters. + const internal = this.id.startsWith('internal/'); + this.exportKeys = internal ? [] : ObjectKeys(this.exports); + } + this.getESMFacade(); + this.syncExports(); + } + return this.exports; } - return nativeModuleRequire(request); -} -// This is exposed for public loaders -NativeModule.prototype.compileForPublicLoader = function(needToSyncExports) { - if (!this.canBeRequiredByUsers) { - // No code because this is an assertion against bugs - // eslint-disable-next-line no-restricted-syntax - throw new Error(`Should not compile ${this.id} for public use`); + getESMFacade() { + if (this.module) return this.module; + const { ModuleWrap } = internalBinding('module_wrap'); + const url = `node:${this.id}`; + const nativeModule = this; + this.module = new ModuleWrap( + url, undefined, [...this.exportKeys, 'default'], + function() { + nativeModule.syncExports(); + this.setExport('default', nativeModule.exports); + }); + // Ensure immediate sync execution to capture exports now + this.module.instantiate(); + this.module.evaluate(-1, false); + return this.module; } - this.compile(); - if (needToSyncExports) { - if (!this.exportKeys) { - // When using --expose-internals, we do not want to reflect the named - // exports from core modules as this can trigger unnecessary getters. - const internal = this.id.startsWith('internal/'); - this.exportKeys = internal ? [] : ObjectKeys(this.exports); + + // 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 updates when this function is + // called so that APMs and other behavior are supported. + syncExports() { + const names = this.exportKeys; + if (this.module) { + for (let i = 0; i < names.length; i++) { + const exportName = names[i]; + if (exportName === 'default') continue; + this.module.setExport(exportName, + getOwn(this.exports, exportName, this.exports)); + } } - this.getESMFacade(); - this.syncExports(); } - return this.exports; -}; -const getOwn = (target, property, receiver) => { - return ObjectPrototypeHasOwnProperty(target, property) ? - ReflectGet(target, property, receiver) : - undefined; -}; + compileForInternalLoader() { + if (this.loaded || this.loading) { + return this.exports; + } -NativeModule.prototype.getURL = function() { - return `node:${this.id}`; -}; + const id = this.id; + this.loading = true; -NativeModule.prototype.getESMFacade = function() { - if (this.module) return this.module; - const { ModuleWrap } = internalBinding('module_wrap'); - const url = this.getURL(); - const nativeModule = this; - this.module = new ModuleWrap( - url, undefined, [...this.exportKeys, 'default'], - function() { - nativeModule.syncExports(); - this.setExport('default', nativeModule.exports); - }); - // Ensure immediate sync execution to capture exports now - this.module.instantiate(); - this.module.evaluate(-1, false); - return this.module; -}; + try { + const requireFn = this.id.startsWith('internal/deps/') ? + requireWithFallbackInDeps : nativeModuleRequire; -// 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 updates when this function is -// called so that APMs and other behavior are supported. -NativeModule.prototype.syncExports = function() { - const names = this.exportKeys; - if (this.module) { - for (let i = 0; i < names.length; i++) { - const exportName = names[i]; - if (exportName === 'default') continue; - this.module.setExport(exportName, - getOwn(this.exports, exportName, this.exports)); + const fn = compileFunction(id); + fn(this.exports, requireFn, this, process, internalBinding, primordials); + + this.loaded = true; + } finally { + this.loading = false; } - } -}; -NativeModule.prototype.compile = function() { - if (this.loaded || this.loading) { + moduleLoadList.push(`NativeModule ${id}`); return this.exports; } +} - const id = this.id; - this.loading = true; +// Think of this as module.exports in this file even though it is not +// written in CommonJS style. +const loaderExports = { + internalBinding, + NativeModule, + require: nativeModuleRequire +}; - try { - const requireFn = this.id.startsWith('internal/deps/') ? - requireWithFallbackInDeps : nativeModuleRequire; +function nativeModuleRequire(id) { + if (id === loaderId) { + return loaderExports; + } - const fn = compileFunction(id); - fn(this.exports, requireFn, this, process, internalBinding, primordials); + const mod = NativeModule.map.get(id); + // Can't load the internal errors module from here, have to use a raw error. + // eslint-disable-next-line no-restricted-syntax + if (!mod) throw new TypeError(`Missing internal module '${id}'`); + return mod.compileForInternalLoader(); +} - this.loaded = true; - } finally { - this.loading = false; +// Allow internal modules from dependencies to require +// other modules from dependencies by providing fallbacks. +function requireWithFallbackInDeps(request) { + if (!NativeModule.map.has(request)) { + request = `internal/deps/${request}`; } + return nativeModuleRequire(request); +} - moduleLoadList.push(`NativeModule ${id}`); - return this.exports; -}; - -// This will be passed to internal/bootstrap/node.js. +// Pass the exports back to C++ land for C++ internals to use. return loaderExports;