Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lib: refactor NativeModule #30856

Closed
wants to merge 1 commit into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
278 changes: 146 additions & 132 deletions lib/internal/bootstrap/loaders.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,166 +137,180 @@ 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 = {};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't a public (or maybe private) field declaration be preferable?

class NativeModule {
  exports = {};
  #loaded = false;
  #loading = false;
  module = undefined;
  exportKeys = undefined;
}

I personally find it more readable than a declaration in the constructor, but that might just be me.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't mind if private fields are used but i think we should keep field initialization in one spot (in this case, the constructor, since it assigns from arguments).

// 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() {
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 (!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() {
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 (!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;