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');
}
```

```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);
73 changes: 73 additions & 0 deletions test/parallel/test-module-entrypoint.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
'use strict';

// Tests that `module.entrypoint` exposes the resolved URL of the entry point
// of the current thread, matching the semantics of `require.main`.

const { spawnPromisified } = require('../common');
const fixtures = require('../common/fixtures');
const assert = require('node:assert');
const { spawnSync } = require('node:child_process');
const { realpathSync } = require('node:fs');
const { test } = require('node:test');
const { pathToFileURL } = require('node:url');

function fixtureURL(...args) {
// The entrypoint is fully resolved, so account for symlinks in the path
// to the fixtures directory.
return pathToFileURL(realpathSync(fixtures.path('module-entrypoint', ...args))).href;
}

test('CommonJS entry point', async () => {
const entry = fixtures.path('module-entrypoint', 'main.cjs');
const { code, stdout } = await spawnPromisified(process.execPath, [entry]);
assert.strictEqual(code, 0);
assert.deepStrictEqual(JSON.parse(stdout), {
entrypoint: fixtureURL('main.cjs'),
matchesMain: true,
});
});

test('ESM entry point', async () => {
const entry = fixtures.path('module-entrypoint', 'main.mjs');
const { code, stdout } = await spawnPromisified(process.execPath, [entry]);
assert.strictEqual(code, 0);
assert.deepStrictEqual(JSON.parse(stdout), {
entrypoint: fixtureURL('main.mjs'),
matchesMain: true,
});
});

test('extensionless entry point is resolved', async () => {
const entry = fixtures.path('module-entrypoint', 'noext');
const { code, stdout } = await spawnPromisified(process.execPath, [entry]);
assert.strictEqual(code, 0);
assert.strictEqual(stdout.trim(), fixtureURL('noext.js'));
});

test('worker threads get their own entrypoint', async () => {
const entry = fixtures.path('module-entrypoint', 'worker-main.mjs');
const { code, stdout } = await spawnPromisified(process.execPath, [entry]);
assert.strictEqual(code, 0);
const result = JSON.parse(stdout);
assert.strictEqual(result.entrypoint, fixtureURL('worker-main.mjs'));
assert.strictEqual(result.fileWorkerEntrypoint, fixtureURL('worker.cjs'));
assert.strictEqual(result.evalWorkerEntrypoint, 'undefined');
assert.match(result.dataURLWorkerEntrypoint, /^data:text\/javascript,/);
});

test('undefined for --eval', async () => {
const { code, stdout } = await spawnPromisified(process.execPath, [
'--eval', 'console.log(String(require("node:module").entrypoint));',
]);
assert.strictEqual(code, 0);
assert.strictEqual(stdout.trim(), 'undefined');
});

test('undefined for STDIN input', () => {
const { status, stdout } = spawnSync(process.execPath, [], {
input: 'console.log(String(require("node:module").entrypoint));',
encoding: 'utf8',
});
assert.strictEqual(status, 0);
assert.strictEqual(stdout.trim(), 'undefined');
});
Loading