Skip to content
Open
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
46 changes: 46 additions & 0 deletions doc/api/module.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,49 @@ const require = createRequire(import.meta.url);
const siblingModule = require('./sibling-module');
```

### `module.entrypoint`

<!-- YAML
added: REPLACEME
-->

* Type: {string|undefined}

The resolved URL of the entry point of the current thread, or `undefined` when
Node.js was started without an entry point script (`--eval`, the REPL, or code
piped via STDIN). It works regardless of whether the entry point is a
CommonJS or an ECMAScript module.

Inside a [worker thread][], `module.entrypoint` is the URL of the script the
worker was started with, matching the semantics of `require.main` and
[`import.meta.main`][], rather than the entry point of the process. Workers
created with `eval: true` have an `undefined` entrypoint.

Unlike `process.argv[1]`, the value is fully resolved: extension searching is
applied, and symlinks are followed unless [`--preserve-symlinks-main`][] is
set. This makes it consistent with the `require.main.filename` of a CommonJS
entry point and the `import.meta.url` of an ECMAScript module entry point.

The value is `undefined` in code that runs before the entry point has been
resolved, such as modules preloaded with `--require`.

```mjs
import { entrypoint } from 'node:module';

if (import.meta.url === entrypoint) {
console.log('This module is the entry point of the current thread');
}
Comment on lines +96 to +100

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

That's not a great example, import.meta.main would be a much better choice here – and if we can't find a good example, do we actually need that API?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

because import.meta.main only returns true if it's the current one, but don't report us what's the actual "entrypoint" of the current thread/module chain. So it's not easily discoverable.

The same can be said for #64800.

If you want I can remove the example.

@aduh95 aduh95 Aug 5, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yeah I think the example either needs to include import.meta.main, and/or process.mainModule (to explain the diffs and show some actual use-cases) otherwise it looks like we're adding a less elegant way to do something that's already possible

```

```cjs
const { entrypoint } = require('node:module');
const { pathToFileURL } = require('node:url');

if (pathToFileURL(__filename).href === entrypoint) {
console.log('This module is the entry point of the current thread');
}
```

### `module.findPackageJSON(specifier[, base])`

<!-- YAML
Expand Down Expand Up @@ -2056,13 +2099,15 @@ returned object contains the following keys:
[`"exports"`]: packages.md#exports
[`--enable-source-maps`]: cli.md#--enable-source-maps
[`--import`]: cli.md#--importmodule
[`--preserve-symlinks-main`]: cli.md#--preserve-symlinks-main
[`--require`]: cli.md#-r---require-module
[`NODE_COMPILE_CACHE=dir`]: cli.md#node_compile_cachedir
[`NODE_COMPILE_CACHE_PORTABLE=1`]: cli.md#node_compile_cache_portable1
[`NODE_DISABLE_COMPILE_CACHE=1`]: cli.md#node_disable_compile_cache1
[`NODE_V8_COVERAGE=dir`]: cli.md#node_v8_coveragedir
[`Object.freeze()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze
[`SourceMap`]: #class-modulesourcemap
[`import.meta.main`]: esm.md#importmetamain
[`initialize`]: #initialize
[`module.constants.compileCacheStatus`]: #moduleconstantscompilecachestatus
[`module.enableCompileCache()`]: #moduleenablecompilecacheoptions
Expand Down Expand Up @@ -2091,3 +2136,4 @@ returned object contains the following keys:
[the documentation of `Worker`]: worker_threads.md#new-workerfilename-options
[transferable objects]: worker_threads.md#portpostmessagevalue-transferlist
[type-stripping]: typescript.md#type-stripping
[worker thread]: worker_threads.md
1 change: 1 addition & 0 deletions lib/internal/main/worker_thread.js
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ port.on('message', (message) => {

case 'data-url': {
const { runEntryPointWithESMLoader } = require('internal/modules/run_main');
require('internal/modules/helpers').setEntrypoint(filename);

RegExpPrototypeExec(/^/, ''); // Necessary to reset RegExp statics before user code runs.
const promise = runEntryPointWithESMLoader((cascadedLoader) => {
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 @@ -518,6 +518,14 @@ function getCompileCacheDir() {
return _getCompileCacheDir() || undefined;
}

/**
* The resolved URL of the entry point of the current thread, if it was
* started with an entry point (i.e. not eval, REPL or STDIN input).
* In worker threads this is the entry point of the worker, not the process.
* @type {string|undefined}
*/
let entrypoint;

function getRequireStack(parent) {
const requireStack = [];
for (let cursor = parent;
Expand Down Expand Up @@ -547,6 +555,12 @@ module.exports = {
stringify,
stripBOM,
toRealPath,
getEntrypoint() {
return entrypoint;
},
setEntrypoint(url) {
entrypoint = url;
},
hasStartedUserCJSExecution() {
return _hasStartedUserCJSExecution;
},
Expand Down
5 changes: 5 additions & 0 deletions lib/internal/modules/run_main.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,15 +146,20 @@ function executeUserEntryPoint(main = process.argv[1]) {
resolvedMain = resolveMainPath(main);
useESMLoader = shouldUseESMLoader(resolvedMain);
}
const { setEntrypoint } = require('internal/modules/helpers');
// Unless we know we should use the ESM loader to handle the entry point per the checks in `shouldUseESMLoader`, first
// try to run the entry point via the CommonJS loader; and if that fails under certain conditions, retry as ESM.
if (!useESMLoader) {
if (resolvedMain !== undefined) {
setEntrypoint(pathToFileURL(resolvedMain).href);
}
const cjsLoader = require('internal/modules/cjs/loader');
const { wrapModuleLoad } = cjsLoader;
wrapModuleLoad(main, null, true);
} else {
const mainPath = resolvedMain || main;
const mainURL = getOptionValue('--entry-url') ? new URL(mainPath, getCWDURL()) : pathToFileURL(mainPath);
setEntrypoint(mainURL.href);

runEntryPointWithESMLoader((cascadedLoader) => {
// Note that if the graph contains unsettled TLA, this may never resolve
Expand Down
12 changes: 12 additions & 0 deletions lib/module.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
'use strict';

const {
ObjectDefineProperty,
} = primordials;

const {
findSourceMap,
getSourceMapsSupport,
Expand All @@ -15,6 +19,7 @@ const {
enableCompileCache,
flushCompileCache,
getCompileCacheDir,
getEntrypoint,
} = require('internal/modules/helpers');
const {
findPackageJSON,
Expand All @@ -29,6 +34,13 @@ Module.flushCompileCache = flushCompileCache;
Module.getCompileCacheDir = getCompileCacheDir;
Module.stripTypeScriptTypes = stripTypeScriptTypes;

ObjectDefineProperty(Module, 'entrypoint', {
__proto__: null,
configurable: true,
enumerable: true,
get: getEntrypoint,
});

// SourceMap APIs
Module.findSourceMap = findSourceMap;
Module.SourceMap = SourceMap;
Expand Down
8 changes: 8 additions & 0 deletions test/fixtures/module-entrypoint/main.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
'use strict';

const { entrypoint } = require('node:module');

console.log(JSON.stringify({
entrypoint,
matchesMain: require('node:url').pathToFileURL(__filename).href === entrypoint,
}));
6 changes: 6 additions & 0 deletions test/fixtures/module-entrypoint/main.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { entrypoint } from 'node:module';

console.log(JSON.stringify({
entrypoint,
matchesMain: import.meta.url === entrypoint,
}));
3 changes: 3 additions & 0 deletions test/fixtures/module-entrypoint/noext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
'use strict';

console.log(require('node:module').entrypoint);
28 changes: 28 additions & 0 deletions test/fixtures/module-entrypoint/worker-main.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { entrypoint } from 'node:module';
import { Worker } from 'node:worker_threads';
import { once } from 'node:events';

const fileWorker = new Worker(new URL('./worker.cjs', import.meta.url));
const [fileWorkerEntrypoint] = await once(fileWorker, 'message');

const evalWorker = new Worker(
'require("node:worker_threads").parentPort.postMessage(' +
'String(require("node:module").entrypoint));',
{ eval: true },
);
const [evalWorkerEntrypoint] = await once(evalWorker, 'message');

const dataURL = 'data:text/javascript,' + encodeURIComponent(
'import { entrypoint } from "node:module";' +
'import { parentPort } from "node:worker_threads";' +
'parentPort.postMessage(entrypoint);',
);
const dataURLWorker = new Worker(new URL(dataURL));
const [dataURLWorkerEntrypoint] = await once(dataURLWorker, 'message');

console.log(JSON.stringify({
entrypoint,
fileWorkerEntrypoint,
evalWorkerEntrypoint,
dataURLWorkerEntrypoint,
}));
6 changes: 6 additions & 0 deletions test/fixtures/module-entrypoint/worker.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
'use strict';

const { entrypoint } = require('node:module');
const { parentPort } = require('node:worker_threads');

parentPort.postMessage(entrypoint);
69 changes: 33 additions & 36 deletions test/parallel/test-dtls-accessors.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@ import {
import assert from 'node:assert';
import * as fixtures from '../common/fixtures.mjs';

const { strictEqual } = assert;
const { readKey } = fixtures;

if (!hasCrypto) {
skip('missing crypto');
}
Expand All @@ -22,9 +19,9 @@ if (!process.features.dtls) {

const { listen, connect } = await import('node:dtls');

const cert = readKey('agent1-cert.pem').toString();
const key = readKey('agent1-key.pem').toString();
const ca = readKey('ca1-cert.pem').toString();
const cert = fixtures.readKey('agent1-cert.pem').toString();
const key = fixtures.readKey('agent1-key.pem').toString();
const ca = fixtures.readKey('ca1-cert.pem').toString();

const gotServerSession = Promise.withResolvers();

Expand All @@ -34,26 +31,26 @@ const server = listen(mustCall((session) => {

// --- Endpoint state after listen(): bound and listening. ---
const es = server.state;
strictEqual(es.bound, true);
strictEqual(es.listening, true);
strictEqual(es.closing, false);
strictEqual(es.destroyed, false);
strictEqual(es.sessionCount, 0);
assert.strictEqual(es.bound, true);
assert.strictEqual(es.listening, true);
assert.strictEqual(es.closing, false);
assert.strictEqual(es.destroyed, false);
assert.strictEqual(es.sessionCount, 0);

// The busy property is settable via the endpoint and reflected in the state view.
strictEqual(server.busy, false);
strictEqual(es.busy, false);
assert.strictEqual(server.busy, false);
assert.strictEqual(es.busy, false);
server.busy = true;
strictEqual(server.busy, true);
strictEqual(es.busy, true);
assert.strictEqual(server.busy, true);
assert.strictEqual(es.busy, true);
server.busy = false;
strictEqual(es.busy, false);
assert.strictEqual(es.busy, false);

// --- Endpoint onerror accessor. ---
strictEqual(server.onerror, undefined);
assert.strictEqual(server.onerror, undefined);
const onEndpointError = mustNotCall();
server.onerror = onEndpointError;
strictEqual(server.onerror, onEndpointError);
assert.strictEqual(server.onerror, onEndpointError);

const client = connect('127.0.0.1', server.address.port, {
ca: [ca],
Expand All @@ -62,43 +59,43 @@ const client = connect('127.0.0.1', server.address.port, {

// --- Session state during the handshake. ---
const cs = client.state;
strictEqual(cs.handshaking, true);
strictEqual(cs.open, false);
strictEqual(cs.closing, false);
strictEqual(cs.destroyed, false);
strictEqual(cs.hasMessageListener, false);
assert.strictEqual(cs.handshaking, true);
assert.strictEqual(cs.open, false);
assert.strictEqual(cs.closing, false);
assert.strictEqual(cs.destroyed, false);
assert.strictEqual(cs.hasMessageListener, false);

// --- Session callback accessors: unset, then set. ---
strictEqual(client.onmessage, undefined);
strictEqual(client.onerror, undefined);
strictEqual(client.onhandshake, undefined);
strictEqual(client.onkeylog, undefined);
assert.strictEqual(client.onmessage, undefined);
assert.strictEqual(client.onerror, undefined);
assert.strictEqual(client.onhandshake, undefined);
assert.strictEqual(client.onkeylog, undefined);
// A connect() session owns its internal endpoint.
strictEqual(client.ownsEndpoint, true);
assert.strictEqual(client.ownsEndpoint, true);

client.onmessage = mustNotCall();
strictEqual(typeof client.onmessage, 'function');
assert.strictEqual(typeof client.onmessage, 'function');
// Attaching a message listener flips the shared flag.
strictEqual(cs.hasMessageListener, true);
assert.strictEqual(cs.hasMessageListener, true);

client.onerror = mustNotCall();
strictEqual(typeof client.onerror, 'function');
assert.strictEqual(typeof client.onerror, 'function');

client.onhandshake = mustCall();
strictEqual(typeof client.onhandshake, 'function');
assert.strictEqual(typeof client.onhandshake, 'function');

client.onkeylog = mustCallAtLeast();
strictEqual(typeof client.onkeylog, 'function');
assert.strictEqual(typeof client.onkeylog, 'function');

await client.opened;

// --- Session state after the handshake completes. ---
strictEqual(cs.handshaking, false);
strictEqual(cs.open, true);
assert.strictEqual(cs.handshaking, false);
assert.strictEqual(cs.open, true);

const serverSession = await gotServerSession.promise;
await serverSession.opened;
strictEqual(es.sessionCount, 1);
assert.strictEqual(es.sessionCount, 1);

await client.close();
await server.close();
4 changes: 2 additions & 2 deletions test/parallel/test-dtls-alpn.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ await endpoint.close();
const serverSession = await gotServerSession.promise;
await serverSession.opened;

strictEqual(client.alpnProtocol, undefined);
strictEqual(serverSession.alpnProtocol, undefined);
assert.strictEqual(client.alpnProtocol, undefined);
assert.strictEqual(serverSession.alpnProtocol, undefined);

await client.close();
await server.close();
Expand Down
Loading
Loading