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

process: add process.getBuiltinModule(id) #52762

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
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
8 changes: 4 additions & 4 deletions doc/api/esm.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,12 +293,12 @@ mandatory:
| ---------------- | ---------------- |
| `'json'` | [JSON modules][] |

## Builtin modules
## Built-in modules

[Core modules][] provide named exports of their public API. A
[Built-in modules][] provide named exports of their public API. A
default export is also provided which is the value of the CommonJS exports.
The default export can be used for, among other things, modifying the named
exports. Named exports of builtin modules are updated only by calling
exports. Named exports of built-in modules are updated only by calling
[`module.syncBuiltinESMExports()`][].

```js
Expand Down Expand Up @@ -1145,8 +1145,8 @@ resolution for ESM specifiers is [commonjs-extension-resolution-loader][].

[6.1.7 Array Index]: https://tc39.es/ecma262/#integer-index
[Addons]: addons.md
[Built-in modules]: modules.md#built-in-modules
[CommonJS]: modules.md
[Core modules]: modules.md#core-modules
[Determining module system]: packages.md#determining-module-system
[Dynamic `import()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import
[ES Module Integration Proposal for WebAssembly]: https://github.com/webassembly/esm-integration
Expand Down
26 changes: 20 additions & 6 deletions doc/api/modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ them as different modules and will reload the file multiple times. For example,
`require('./foo')` and `require('./FOO')` return two different objects,
irrespective of whether or not `./foo` and `./FOO` are the same file.

## Core modules
## Built-in modules

<!--type=misc-->

Expand All @@ -396,20 +396,31 @@ changes:
Node.js has several modules compiled into the binary. These modules are
described in greater detail elsewhere in this documentation.

The core modules are defined within the Node.js source and are located in the
The built-in modules are defined within the Node.js source and are located in the
`lib/` folder.

Core modules can be identified using the `node:` prefix, in which case
built-in modules can be identified using the `node:` prefix, in which case
it bypasses the `require` cache. For instance, `require('node:http')` will
always return the built in HTTP module, even if there is `require.cache` entry
by that name.

Some core modules are always preferentially loaded if their identifier is
Some built-in modules are always preferentially loaded if their identifier is
passed to `require()`. For instance, `require('http')` will always
return the built-in HTTP module, even if there is a file by that name. The list
of core modules that can be loaded without using the `node:` prefix is exposed
of built-in modules that can be loaded without using the `node:` prefix is exposed
as [`module.builtinModules`][].

### Built-in modules with mandatory `node:` prefix

When being loaded by `require()`, some built-in modules must be requested with the
`node:` prefix. This requirement exists to prevent newly introduced built-in
modules from having a conflict with user land packages that already have
taken the name. Currently the built-in modules that requires the `node:` prefix are:

* [`node:sea`][]
* [`node:test`][]
* [`node:test/reporters`][]

## Cycles

<!--type=misc-->
Expand Down Expand Up @@ -552,7 +563,7 @@ folders as modules, and work for both `require` and `import`.
<!--type=misc-->

If the module identifier passed to `require()` is not a
[core](#core-modules) module, and does not begin with `'/'`, `'../'`, or
[built-in](#built-in-modules) module, and does not begin with `'/'`, `'../'`, or
`'./'`, then Node.js starts at the directory of the current module, and
adds `/node_modules`, and attempts to load the module from that location.
Node.js will not append `node_modules` to a path already ending in
Expand Down Expand Up @@ -1166,6 +1177,9 @@ This section was moved to
[`module.id`]: #moduleid
[`module` core module]: module.md
[`module` object]: #the-module-object
[`node:sea`]: single-executable-applications.md#single-executable-application-api
[`node:test/reporters`]: test.md#test-reporters
[`node:test`]: test.md
[`package.json`]: packages.md#nodejs-packagejson-field-definitions
[`path.dirname()`]: path.md#pathdirnamepath
[`require.main`]: #requiremain
Expand Down
43 changes: 43 additions & 0 deletions doc/api/process.md
Original file line number Diff line number Diff line change
Expand Up @@ -1917,6 +1917,46 @@ console.log('After:', getActiveResourcesInfo());
// After: [ 'TTYWrap', 'TTYWrap', 'TTYWrap', 'Timeout' ]
```

## `process.getBuiltinModule(id)`

<!-- YAML
added: REPLACEME
-->

* `id` {string} ID of the built-in module being requested.
* Returns: {Object|undefined}

`process.getBuiltinModule(id)` provides a way to load built-in modules
in a globally available function. ES Modules that need to support
other environments can use it to conditionally load a Node.js built-in
when it is run in Node.js, without having to deal with the resolution
error that can be thrown by `import` in a non-Node.js environment or
having to use dynamic `import()` which either turns the module into
an asynchronous module, or turns a synchronous API into an asynchronous one.

```mjs
if (globalThis.process?.getBuiltinModule) {
// Run in Node.js, use the Node.js fs module.
const fs = globalThis.process.getBuiltinModule('fs');
// If `require()` is needed to load user-modules, use createRequire()
const module = globalThis.process.getBuiltinModule('module');
const require = module.createRequire(import.meta.url);
const foo = require('foo');
}
```

If `id` specifies a built-in module available in the current Node.js process,
`process.getBuiltinModule(id)` method returns the corresponding built-in
module. If `id` does not correspond to any built-in module, `undefined`
is returned.

`process.getBuiltinModule(id)` accepts built-in module IDs that are recognized
by [`module.isBuiltin(id)`][]. Some built-in modules must be loaded with the
`node:` prefix, see [built-in modules with mandatory `node:` prefix][].
The references returned by `process.getBuiltinModule(id)` always point to
the built-in module corresponding to `id` even if users modify
[`require.cache`][] so that `require(id)` returns something else.

## `process.getegid()`

<!-- YAML
Expand Down Expand Up @@ -4020,6 +4060,7 @@ cases:
[`console.error()`]: console.md#consoleerrordata-args
[`console.log()`]: console.md#consolelogdata-args
[`domain`]: domain.md
[`module.isBuiltin(id)`]: module.md#moduleisbuiltinmodulename
[`net.Server`]: net.md#class-netserver
[`net.Socket`]: net.md#class-netsocket
[`os.constants.dlopen`]: os.md#dlopen-constants
Expand All @@ -4036,9 +4077,11 @@ cases:
[`queueMicrotask()`]: globals.md#queuemicrotaskcallback
[`readable.read()`]: stream.md#readablereadsize
[`require()`]: globals.md#require
[`require.cache`]: modules.md#requirecache
[`require.main`]: modules.md#accessing-the-main-module
[`subprocess.kill()`]: child_process.md#subprocesskillsignal
[`v8.setFlagsFromString()`]: v8.md#v8setflagsfromstringflags
[built-in modules with mandatory `node:` prefix]: modules.md#built-in-modules-with-mandatory-node-prefix
[debugger]: debugger.md
[deprecation code]: deprecations.md
[note on process I/O]: #a-note-on-process-io
Expand Down
5 changes: 5 additions & 0 deletions lib/internal/bootstrap/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,11 @@ internalBinding('process_methods').setEmitWarningSync(emitWarningSync);
setMaybeCacheGeneratedSourceMap(maybeCacheGeneratedSourceMap);
}

{
const { getBuiltinModule } = require('internal/modules/helpers');
process.getBuiltinModule = getBuiltinModule;
}

function setupProcessObject() {
const EventEmitter = require('events');
const origProcProto = ObjectGetPrototypeOf(process);
Expand Down
14 changes: 14 additions & 0 deletions lib/internal/modules/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -342,8 +342,22 @@ let _hasStartedUserCJSExecution = false;
// there is little value checking whether any user JS code is run anyway.
let _hasStartedUserESMExecution = false;

/**
* Load a public built-in module. ID may or may not be prefixed by `node:` and
* will be normalized.
* @param {string} id ID of the built-in to be loaded.
* @returns {object|undefined} exports of the built-in. Undefined if the built-in
* does not exist.
*/
function getBuiltinModule(id) {
validateString(id, 'id');
const normalizedId = BuiltinModule.normalizeRequirableId(id);
return normalizedId ? require(normalizedId) : undefined;
}

module.exports = {
addBuiltinLibsToObject,
getBuiltinModule,
getCjsConditions,
initializeCjsConditions,
loadBuiltinModule,
Expand Down
2 changes: 2 additions & 0 deletions test/common/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const {
getCallSite,
getTTYfd,
hasCrypto,
hasIntl,
hasIPv6,
hasMultiLocalhost,
isAIX,
Expand Down Expand Up @@ -72,6 +73,7 @@ export {
getPort,
getTTYfd,
hasCrypto,
hasIntl,
hasIPv6,
hasMultiLocalhost,
isAIX,
Expand Down
55 changes: 55 additions & 0 deletions test/parallel/test-process-get-builtin.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { isMainThread, hasCrypto, hasIntl } from '../common/index.mjs';
import assert from 'node:assert';
import { builtinModules } from 'node:module';

for (const invalid of [1, undefined, null, false, [], {}, () => {}, Symbol('test')]) {
assert.throws(() => process.getBuiltinModule(invalid), { code: 'ERR_INVALID_ARG_TYPE' });
}

for (const invalid of [
'invalid', 'test', 'sea', 'test/reporter', 'internal/bootstrap/realm',
'internal/deps/undici/undici', 'internal/util',
]) {
assert.strictEqual(process.getBuiltinModule(invalid), undefined);
}

// Check that createRequire()(id) returns the same thing as process.getBuiltinModule(id).
const require = process.getBuiltinModule('module').createRequire(import.meta.url);
const publicBuiltins = new Set(builtinModules);

// Remove built-ins not available in the current setup.
if (!isMainThread) {
publicBuiltins.delete('trace_events');
}
if (!hasCrypto) {
publicBuiltins.delete('crypto');
publicBuiltins.delete('tls');
publicBuiltins.delete('_tls_common');
publicBuiltins.delete('_tls_wrap');
publicBuiltins.delete('http2');
publicBuiltins.delete('https');
publicBuiltins.delete('inspector');
publicBuiltins.delete('inspector/promises');
}
if (!hasIntl) {
publicBuiltins.delete('inspector');
publicBuiltins.delete('trace_events');
}

for (const id of publicBuiltins) {
assert.strictEqual(process.getBuiltinModule(id), require(id));
}
// Check that import(id).default returns the same thing as process.getBuiltinModule(id).
for (const id of publicBuiltins) {
const imported = await import(`node:${id}`);
assert.strictEqual(process.getBuiltinModule(id), imported.default);
}

// publicBuiltins does not include 'test' which requires the node: prefix.
const ids = publicBuiltins.add('test');
// Check that import(id).default returns the same thing as process.getBuiltinModule(id).
for (const id of ids) {
const prefixed = `node:${id}`;
const imported = await import(prefixed);
assert.strictEqual(process.getBuiltinModule(prefixed), imported.default);
}