Skip to content

Commit 1c6e2ec

Browse files
joyeecheungtargos
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 5b78bf2 commit 1c6e2ec

File tree

1 file changed

+147
-133
lines changed

1 file changed

+147
-133
lines changed

lib/internal/bootstrap/loaders.js

+147-133
Original file line numberDiff line numberDiff line change
@@ -140,168 +140,182 @@ let internalBinding;
140140
};
141141
}
142142

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

181-
NativeModule.map = new Map();
182-
for (let i = 0; i < moduleIds.length; ++i) {
183-
const id = moduleIds[i];
184-
const mod = new NativeModule(id);
185-
NativeModule.map.set(id, mod);
186-
}
149+
const getOwn = (target, property, receiver) => {
150+
return ObjectPrototypeHasOwnProperty(target, property) ?
151+
ReflectGet(target, property, receiver) :
152+
undefined;
153+
};
187154

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

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

200-
NativeModule.exists = function(id) {
201-
return NativeModule.map.has(id);
202-
};
203+
static exists(id) {
204+
return NativeModule.map.has(id);
205+
}
203206

204-
NativeModule.canBeRequiredByUsers = function(id) {
205-
const mod = NativeModule.map.get(id);
206-
return mod && mod.canBeRequiredByUsers;
207-
};
207+
static canBeRequiredByUsers(id) {
208+
const mod = NativeModule.map.get(id);
209+
return mod && mod.canBeRequiredByUsers;
210+
}
208211

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

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

239-
const getOwn = (target, property, receiver) => {
240-
return ObjectPrototypeHasOwnProperty(target, property) ?
241-
ReflectGet(target, property, receiver) :
242-
undefined;
243-
};
266+
compileForInternalLoader() {
267+
if (this.loaded || this.loading) {
268+
return this.exports;
269+
}
244270

245-
NativeModule.prototype.getURL = function() {
246-
return `node:${this.id}`;
247-
};
271+
const id = this.id;
272+
this.loading = true;
248273

249-
NativeModule.prototype.getESMFacade = function() {
250-
if (this.module) return this.module;
251-
const { ModuleWrap } = internalBinding('module_wrap');
252-
const url = this.getURL();
253-
const nativeModule = this;
254-
this.module = new ModuleWrap(
255-
url, undefined, [...this.exportKeys, 'default'],
256-
function() {
257-
nativeModule.syncExports();
258-
this.setExport('default', nativeModule.exports);
259-
});
260-
// Ensure immediate sync execution to capture exports now
261-
this.module.instantiate();
262-
this.module.evaluate(-1, false);
263-
return this.module;
264-
};
274+
try {
275+
const requireFn = this.id.startsWith('internal/deps/') ?
276+
requireWithFallbackInDeps : nativeModuleRequire;
265277

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

282-
NativeModule.prototype.compile = function() {
283-
if (this.loaded || this.loading) {
286+
moduleLoadList.push(`NativeModule ${id}`);
284287
return this.exports;
285288
}
289+
}
286290

287-
const id = this.id;
288-
this.loading = true;
291+
// Think of this as module.exports in this file even though it is not
292+
// written in CommonJS style.
293+
const loaderExports = {
294+
internalBinding,
295+
NativeModule,
296+
require: nativeModuleRequire
297+
};
289298

290-
try {
291-
const requireFn = this.id.startsWith('internal/deps/') ?
292-
requireWithFallbackInDeps : nativeModuleRequire;
299+
function nativeModuleRequire(id) {
300+
if (id === loaderId) {
301+
return loaderExports;
302+
}
293303

294-
const fn = compileFunction(id);
295-
fn(this.exports, requireFn, this, process, internalBinding, primordials);
304+
const mod = NativeModule.map.get(id);
305+
// Can't load the internal errors module from here, have to use a raw error.
306+
// eslint-disable-next-line no-restricted-syntax
307+
if (!mod) throw new TypeError(`Missing internal module '${id}'`);
308+
return mod.compileForInternalLoader();
309+
}
296310

297-
this.loaded = true;
298-
} finally {
299-
this.loading = false;
311+
// Allow internal modules from dependencies to require
312+
// other modules from dependencies by providing fallbacks.
313+
function requireWithFallbackInDeps(request) {
314+
if (!NativeModule.map.has(request)) {
315+
request = `internal/deps/${request}`;
300316
}
317+
return nativeModuleRequire(request);
318+
}
301319

302-
moduleLoadList.push(`NativeModule ${id}`);
303-
return this.exports;
304-
};
305-
306-
// This will be passed to internal/bootstrap/node.js.
320+
// Pass the exports back to C++ land for C++ internals to use.
307321
return loaderExports;

0 commit comments

Comments
 (0)