Skip to content

Commit

Permalink
lib: refactor NativeModule
Browse files Browse the repository at this point in the history
Refactor the internal NativeModule class to a JS class and add
more documentation about its properties.

PR-URL: #30856
Reviewed-By: Denys Otrishko <shishugi@gmail.com>
Reviewed-By: Gus Caplan <me@gus.host>
Reviewed-By: Rich Trott <rtrott@gmail.com>
  • Loading branch information
joyeecheung authored and BethGriggs committed Feb 6, 2020
1 parent 5fff46a commit 84c9e4f
Showing 1 changed file with 147 additions and 133 deletions.
280 changes: 147 additions & 133 deletions lib/internal/bootstrap/loaders.js
Expand Up @@ -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<string, NativeModule>}
*/
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;

0 comments on commit 84c9e4f

Please sign in to comment.