Skip to content

Commit 30d3249

Browse files
joyeecheungMylesBorins
authored andcommitted
lib: refactor NativeModule
Refactor the internal NativeModule class to a JS class and add more documentation about its properties. PR-URL: #30856 Reviewed-By: Denys Otrishko <[email protected]> Reviewed-By: Gus Caplan <[email protected]> Reviewed-By: Rich Trott <[email protected]>
1 parent 60225c1 commit 30d3249

File tree

1 file changed

+146
-132
lines changed

1 file changed

+146
-132
lines changed

lib/internal/bootstrap/loaders.js

+146-132
Original file line numberDiff line numberDiff line change
@@ -137,166 +137,180 @@ let internalBinding;
137137
};
138138
}
139139

140-
// Think of this as module.exports in this file even though it is not
141-
// written in CommonJS style.
142-
const loaderExports = {
143-
internalBinding,
144-
NativeModule,
145-
require: nativeModuleRequire
146-
};
147-
148140
const loaderId = 'internal/bootstrap/loaders';
149-
150-
// Set up NativeModule.
151-
function NativeModule(id) {
152-
this.filename = `${id}.js`;
153-
this.id = id;
154-
this.exports = {};
155-
this.module = undefined;
156-
this.exportKeys = undefined;
157-
this.loaded = false;
158-
this.loading = false;
159-
this.canBeRequiredByUsers = !id.startsWith('internal/');
160-
}
161-
162-
// To be called during pre-execution when --expose-internals is on.
163-
// Enables the user-land module loader to access internal modules.
164-
NativeModule.exposeInternals = function() {
165-
for (const [id, mod] of NativeModule.map) {
166-
// Do not expose this to user land even with --expose-internals.
167-
if (id !== loaderId) {
168-
mod.canBeRequiredByUsers = true;
169-
}
170-
}
171-
};
172-
173141
const {
174142
moduleIds,
175143
compileFunction
176144
} = internalBinding('native_module');
177145

178-
NativeModule.map = new Map();
179-
for (let i = 0; i < moduleIds.length; ++i) {
180-
const id = moduleIds[i];
181-
const mod = new NativeModule(id);
182-
NativeModule.map.set(id, mod);
183-
}
146+
const getOwn = (target, property, receiver) => {
147+
return ObjectPrototypeHasOwnProperty(target, property) ?
148+
ReflectGet(target, property, receiver) :
149+
undefined;
150+
};
184151

185-
function nativeModuleRequire(id) {
186-
if (id === loaderId) {
187-
return loaderExports;
152+
/**
153+
* An internal abstraction for the built-in JavaScript modules of Node.js.
154+
* Be careful not to expose this to user land unless --expose-internals is
155+
* used, in which case there is no compatibility guarantee about this class.
156+
*/
157+
class NativeModule {
158+
/**
159+
* A map from the module IDs to the module instances.
160+
* @type {Map<string, NativeModule>}
161+
*/
162+
static map = new Map(moduleIds.map((id) => [id, new NativeModule(id)]));
163+
164+
constructor(id) {
165+
this.filename = `${id}.js`;
166+
this.id = id;
167+
this.canBeRequiredByUsers = !id.startsWith('internal/');
168+
169+
// The CJS exports object of the module.
170+
this.exports = {};
171+
// States used to work around circular dependencies.
172+
this.loaded = false;
173+
this.loading = false;
174+
175+
// The following properties are used by the ESM implementation and only
176+
// initialized when the native module is loaded by users.
177+
/**
178+
* The C++ ModuleWrap binding used to interface with the ESM implementation.
179+
* @type {ModuleWrap|undefined}
180+
*/
181+
this.module = undefined;
182+
/**
183+
* Exported names for the ESM imports.
184+
* @type {string[]|undefined}
185+
*/
186+
this.exportKeys = undefined;
188187
}
189188

190-
const mod = NativeModule.map.get(id);
191-
// Can't load the internal errors module from here, have to use a raw error.
192-
// eslint-disable-next-line no-restricted-syntax
193-
if (!mod) throw new TypeError(`Missing internal module '${id}'`);
194-
return mod.compile();
195-
}
189+
// To be called during pre-execution when --expose-internals is on.
190+
// Enables the user-land module loader to access internal modules.
191+
static exposeInternals() {
192+
for (const [id, mod] of NativeModule.map) {
193+
// Do not expose this to user land even with --expose-internals.
194+
if (id !== loaderId) {
195+
mod.canBeRequiredByUsers = true;
196+
}
197+
}
198+
}
196199

197-
NativeModule.exists = function(id) {
198-
return NativeModule.map.has(id);
199-
};
200+
static exists(id) {
201+
return NativeModule.map.has(id);
202+
}
200203

201-
NativeModule.canBeRequiredByUsers = function(id) {
202-
const mod = NativeModule.map.get(id);
203-
return mod && mod.canBeRequiredByUsers;
204-
};
204+
static canBeRequiredByUsers(id) {
205+
const mod = NativeModule.map.get(id);
206+
return mod && mod.canBeRequiredByUsers;
207+
}
205208

206-
// Allow internal modules from dependencies to require
207-
// other modules from dependencies by providing fallbacks.
208-
function requireWithFallbackInDeps(request) {
209-
if (!NativeModule.map.has(request)) {
210-
request = `internal/deps/${request}`;
209+
// Used by user-land module loaders to compile and load builtins.
210+
compileForPublicLoader() {
211+
if (!this.canBeRequiredByUsers) {
212+
// No code because this is an assertion against bugs
213+
// eslint-disable-next-line no-restricted-syntax
214+
throw new Error(`Should not compile ${this.id} for public use`);
215+
}
216+
this.compileForInternalLoader();
217+
if (!this.exportKeys) {
218+
// When using --expose-internals, we do not want to reflect the named
219+
// exports from core modules as this can trigger unnecessary getters.
220+
const internal = this.id.startsWith('internal/');
221+
this.exportKeys = internal ? [] : ObjectKeys(this.exports);
222+
}
223+
this.getESMFacade();
224+
this.syncExports();
225+
return this.exports;
211226
}
212-
return nativeModuleRequire(request);
213-
}
214227

215-
// This is exposed for public loaders
216-
NativeModule.prototype.compileForPublicLoader = function() {
217-
if (!this.canBeRequiredByUsers) {
218-
// No code because this is an assertion against bugs
219-
// eslint-disable-next-line no-restricted-syntax
220-
throw new Error(`Should not compile ${this.id} for public use`);
228+
getESMFacade() {
229+
if (this.module) return this.module;
230+
const { ModuleWrap } = internalBinding('module_wrap');
231+
const url = `node:${this.id}`;
232+
const nativeModule = this;
233+
this.module = new ModuleWrap(
234+
url, undefined, [...this.exportKeys, 'default'],
235+
function() {
236+
nativeModule.syncExports();
237+
this.setExport('default', nativeModule.exports);
238+
});
239+
// Ensure immediate sync execution to capture exports now
240+
this.module.instantiate();
241+
this.module.evaluate(-1, false);
242+
return this.module;
221243
}
222-
this.compile();
223-
if (!this.exportKeys) {
224-
// When using --expose-internals, we do not want to reflect the named
225-
// exports from core modules as this can trigger unnecessary getters.
226-
const internal = this.id.startsWith('internal/');
227-
this.exportKeys = internal ? [] : ObjectKeys(this.exports);
244+
245+
// Provide named exports for all builtin libraries so that the libraries
246+
// may be imported in a nicer way for ESM users. The default export is left
247+
// as the entire namespace (module.exports) and updates when this function is
248+
// called so that APMs and other behavior are supported.
249+
syncExports() {
250+
const names = this.exportKeys;
251+
if (this.module) {
252+
for (let i = 0; i < names.length; i++) {
253+
const exportName = names[i];
254+
if (exportName === 'default') continue;
255+
this.module.setExport(exportName,
256+
getOwn(this.exports, exportName, this.exports));
257+
}
258+
}
228259
}
229-
this.getESMFacade();
230-
this.syncExports();
231-
return this.exports;
232-
};
233260

234-
const getOwn = (target, property, receiver) => {
235-
return ObjectPrototypeHasOwnProperty(target, property) ?
236-
ReflectGet(target, property, receiver) :
237-
undefined;
238-
};
261+
compileForInternalLoader() {
262+
if (this.loaded || this.loading) {
263+
return this.exports;
264+
}
239265

240-
NativeModule.prototype.getURL = function() {
241-
return `node:${this.id}`;
242-
};
266+
const id = this.id;
267+
this.loading = true;
243268

244-
NativeModule.prototype.getESMFacade = function() {
245-
if (this.module) return this.module;
246-
const { ModuleWrap } = internalBinding('module_wrap');
247-
const url = this.getURL();
248-
const nativeModule = this;
249-
this.module = new ModuleWrap(
250-
url, undefined, [...this.exportKeys, 'default'],
251-
function() {
252-
nativeModule.syncExports();
253-
this.setExport('default', nativeModule.exports);
254-
});
255-
// Ensure immediate sync execution to capture exports now
256-
this.module.instantiate();
257-
this.module.evaluate(-1, false);
258-
return this.module;
259-
};
269+
try {
270+
const requireFn = this.id.startsWith('internal/deps/') ?
271+
requireWithFallbackInDeps : nativeModuleRequire;
260272

261-
// Provide named exports for all builtin libraries so that the libraries
262-
// may be imported in a nicer way for ESM users. The default export is left
263-
// as the entire namespace (module.exports) and updates when this function is
264-
// called so that APMs and other behavior are supported.
265-
NativeModule.prototype.syncExports = function() {
266-
const names = this.exportKeys;
267-
if (this.module) {
268-
for (let i = 0; i < names.length; i++) {
269-
const exportName = names[i];
270-
if (exportName === 'default') continue;
271-
this.module.setExport(exportName,
272-
getOwn(this.exports, exportName, this.exports));
273+
const fn = compileFunction(id);
274+
fn(this.exports, requireFn, this, process, internalBinding, primordials);
275+
276+
this.loaded = true;
277+
} finally {
278+
this.loading = false;
273279
}
274-
}
275-
};
276280

277-
NativeModule.prototype.compile = function() {
278-
if (this.loaded || this.loading) {
281+
moduleLoadList.push(`NativeModule ${id}`);
279282
return this.exports;
280283
}
284+
}
281285

282-
const id = this.id;
283-
this.loading = true;
286+
// Think of this as module.exports in this file even though it is not
287+
// written in CommonJS style.
288+
const loaderExports = {
289+
internalBinding,
290+
NativeModule,
291+
require: nativeModuleRequire
292+
};
284293

285-
try {
286-
const requireFn = this.id.startsWith('internal/deps/') ?
287-
requireWithFallbackInDeps : nativeModuleRequire;
294+
function nativeModuleRequire(id) {
295+
if (id === loaderId) {
296+
return loaderExports;
297+
}
288298

289-
const fn = compileFunction(id);
290-
fn(this.exports, requireFn, this, process, internalBinding, primordials);
299+
const mod = NativeModule.map.get(id);
300+
// Can't load the internal errors module from here, have to use a raw error.
301+
// eslint-disable-next-line no-restricted-syntax
302+
if (!mod) throw new TypeError(`Missing internal module '${id}'`);
303+
return mod.compileForInternalLoader();
304+
}
291305

292-
this.loaded = true;
293-
} finally {
294-
this.loading = false;
306+
// Allow internal modules from dependencies to require
307+
// other modules from dependencies by providing fallbacks.
308+
function requireWithFallbackInDeps(request) {
309+
if (!NativeModule.map.has(request)) {
310+
request = `internal/deps/${request}`;
295311
}
312+
return nativeModuleRequire(request);
313+
}
296314

297-
moduleLoadList.push(`NativeModule ${id}`);
298-
return this.exports;
299-
};
300-
301-
// This will be passed to internal/bootstrap/node.js.
315+
// Pass the exports back to C++ land for C++ internals to use.
302316
return loaderExports;

0 commit comments

Comments
 (0)