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
50 changes: 50 additions & 0 deletions doc/api/module.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,53 @@ 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`.

Unlike [`import.meta.main`][], which only returns a boolean, `module.entrypoint`
returns the resolved URL string of the entry point. It can be used as a base
for resolving paths relative to the entry point, regardless of whether the
entry point is a CommonJS or an ECMAScript module:

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

// Use the entry point's URL as a base for resolving sibling modules.
const base = new URL('./', entrypoint);
```

In CommonJS, the equivalent is:

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

// Use the entry point's URL as a base for resolving sibling modules.
const base = new URL('./', entrypoint);
```

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

<!-- YAML
Expand Down Expand Up @@ -2112,6 +2159,7 @@ 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
Expand All @@ -2120,6 +2168,7 @@ returned object contains the following keys:
[`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 @@ -2148,3 +2197,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 @@ -227,6 +227,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 @@ -657,6 +657,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 @@ -724,6 +732,12 @@ module.exports = {
setLoaderOverrides,
stringify,
stripBOM,
getEntrypoint() {
return entrypoint;
},
setEntrypoint(url) {
entrypoint = url;
},
hasStartedUserCJSExecution() {
return _hasStartedUserCJSExecution;
},
Expand Down
6 changes: 5 additions & 1 deletion lib/internal/modules/run_main.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const {
const { getOptionValue } = require('internal/options');
// Through loaderMethods rather than the `modules` binding directly: a mounted
// VFS overrides these, and a worker entry point can live inside one.
const { loaderMethods } = require('internal/modules/helpers');
const { loaderMethods, setEntrypoint } = require('internal/modules/helpers');
const path = require('path');
const { pathToFileURL, URL } = require('internal/url');
const { kEmptyObject, getCWDURL } = require('internal/util');
Expand Down Expand Up @@ -168,12 +168,16 @@ function executeUserEntryPoint(main = process.argv[1]) {
// 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);

// When no async loader hooks or --import are registered, load the entry point
// directly. The graph is loaded and linked synchronously, and evaluated
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);
76 changes: 76 additions & 0 deletions test/parallel/test-module-entrypoint.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
'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) {
// Unlike fixtures.fileURL(), this resolves symlinks in the path to the
// fixtures directory: module.entrypoint is always fully resolved (unless
// --preserve-symlinks-main is set), so the expected URL needs to be too,
// otherwise this test would be flaky on checkouts that symlink their way
// 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.deepStrictEqual(JSON.parse(stdout), {
entrypoint: fixtureURL('main.cjs'),
matchesMain: true,
});
assert.strictEqual(code, 0);
});

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

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

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]);
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,/);
assert.strictEqual(code, 0);
});

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

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