esm: implement import.meta.main

Boolean value to check if an ES Module is the entrypoint of the
current process.

Implements: #57226

Co-authored-by: Antoine du Hamel <duhamelantoine1995@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/57804
Fixes: https://github.com/nodejs/node/issues/57226
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Guy Bedford <guybedford@gmail.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>
This commit is contained in:
Joe
2025-05-26 23:31:54 +01:00
committed by GitHub
parent e7255b6bab
commit bbc059378b
11 changed files with 173 additions and 12 deletions

View File

@@ -400,6 +400,35 @@ import { readFileSync } from 'node:fs';
const buffer = readFileSync(new URL('./data.proto', import.meta.url));
```
### `import.meta.main`
<!-- YAML
added:
- REPLACEME
-->
> Stability: 1.0 - Early development
* {boolean} `true` when the current module is the entry point of the current process; `false` otherwise.
Equivalent to `require.main === module` in CommonJS.
Analogous to Python's `__name__ == "__main__"`.
```js
export function foo() {
return 'Hello, world';
}
function main() {
const message = foo();
console.log(message);
}
if (import.meta.main) main();
// `foo` can be imported from another module without possible side-effects from `main`
```
### `import.meta.resolve(specifier)`
<!-- YAML
@@ -616,6 +645,10 @@ These CommonJS variables are not available in ES modules.
They can instead be loaded with [`module.createRequire()`][] or
[`process.dlopen`][].
#### No `require.main`
To replace `require.main === module`, there is the [`import.meta.main`][] API.
#### No `require.resolve`
Relative resolution can be handled via `new URL('./local', import.meta.url)`.
@@ -1181,6 +1214,7 @@ resolution for ESM specifiers is [commonjs-extension-resolution-loader][].
[`import()`]: #import-expressions
[`import.meta.dirname`]: #importmetadirname
[`import.meta.filename`]: #importmetafilename
[`import.meta.main`]: #importmetamain
[`import.meta.resolve`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import.meta/resolve
[`import.meta.url`]: #importmetaurl
[`import`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import

View File

@@ -203,6 +203,20 @@ port.on('message', (message) => {
break;
}
case 'data-url': {
const { runEntryPointWithESMLoader } = require('internal/modules/run_main');
RegExpPrototypeExec(/^/, ''); // Necessary to reset RegExp statics before user code runs.
const promise = runEntryPointWithESMLoader((cascadedLoader) => {
return cascadedLoader.import(filename, undefined, { __proto__: null }, undefined, true);
});
PromisePrototypeThen(promise, undefined, (e) => {
workerOnGlobalUncaughtException(e, true);
});
break;
}
default: {
// script filename
// runMain here might be monkey-patched by users in --require.

View File

@@ -51,12 +51,12 @@ function createImportMetaResolve(defaultParentURL, loader, allowParentURL) {
/**
* Create the `import.meta` object for a module.
* @param {object} meta
* @param {{url: string}} context
* @param {{url: string, isMain?: boolean}} context
* @param {typeof import('./loader.js').ModuleLoader} loader Reference to the current module loader
* @returns {{dirname?: string, filename?: string, url: string, resolve?: Function}}
*/
function initializeImportMeta(meta, context, loader) {
const { url } = context;
const { url, isMain } = context;
// Alphabetical
if (StringPrototypeStartsWith(url, 'file:') === true) {
@@ -65,6 +65,8 @@ function initializeImportMeta(meta, context, loader) {
setLazyPathHelpers(meta, url);
}
meta.main = !!isMain;
if (!loader || loader.allowImportMetaResolve) {
meta.resolve = createImportMetaResolve(url, loader, experimentalImportMetaResolve);
}

View File

@@ -248,10 +248,11 @@ class ModuleLoader {
*
* @param {string} source Source code of the module.
* @param {string} url URL of the module.
* @param {{ isMain?: boolean }|undefined} context - context object containing module metadata.
* @returns {object} The module wrap object.
*/
createModuleWrap(source, url) {
return compileSourceTextModule(url, source, this);
createModuleWrap(source, url, context = kEmptyObject) {
return compileSourceTextModule(url, source, this, context);
}
/**
@@ -289,7 +290,8 @@ class ModuleLoader {
* @returns {Promise<object>} The module object.
*/
eval(source, url, isEntryPoint = false) {
const wrap = this.createModuleWrap(source, url);
const context = isEntryPoint ? { isMain: true } : undefined;
const wrap = this.createModuleWrap(source, url, context);
return this.executeModuleJob(url, wrap, isEntryPoint);
}

View File

@@ -103,7 +103,8 @@ translators.set('module', function moduleStrategy(url, source, isMain) {
source = stringify(source);
debug(`Translating StandardModule ${url}`);
const { compileSourceTextModule } = require('internal/modules/esm/utils');
const module = compileSourceTextModule(url, source, this);
const context = isMain ? { isMain } : undefined;
const module = compileSourceTextModule(url, source, this, context);
return module;
});

View File

@@ -42,6 +42,7 @@ const {
const {
emitExperimentalWarning,
getCWDURL,
kEmptyObject,
} = require('internal/util');
const assert = require('internal/assert');
const {
@@ -188,7 +189,7 @@ function registerModule(referrer, registry) {
*/
function defaultInitializeImportMetaForModule(meta, wrap) {
const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
return cascadedLoader.importMetaInitialize(meta, { url: wrap.url });
return cascadedLoader.importMetaInitialize(meta, { url: wrap.url, isMain: wrap.isMain });
}
/**
@@ -342,15 +343,22 @@ async function initializeHooks() {
* @param {string} source Source code of the module.
* @param {typeof import('./loader.js').ModuleLoader|undefined} cascadedLoader If provided,
* register the module for default handling.
* @param {{ isMain?: boolean }|undefined} context - context object containing module metadata.
* @returns {ModuleWrap}
*/
function compileSourceTextModule(url, source, cascadedLoader) {
function compileSourceTextModule(url, source, cascadedLoader, context = kEmptyObject) {
const hostDefinedOption = cascadedLoader ? source_text_module_default_hdo : undefined;
const wrap = new ModuleWrap(url, undefined, source, 0, 0, hostDefinedOption);
if (!cascadedLoader) {
return wrap;
}
const { isMain } = context;
if (isMain) {
wrap.isMain = true;
}
// Cache the source map for the module if present.
if (wrap.sourceMapURL) {
maybeCacheSourceMap(url, source, wrap, false, undefined, wrap.sourceMapURL);

View File

@@ -7,7 +7,6 @@ const {
AtomicsAdd,
Float64Array,
FunctionPrototypeBind,
JSONStringify,
MathMax,
ObjectEntries,
Promise,
@@ -167,8 +166,8 @@ class Worker extends EventEmitter {
doEval = 'classic';
} else if (isURL(filename) && filename.protocol === 'data:') {
url = null;
doEval = 'module';
filename = `import ${JSONStringify(`${filename}`)}`;
doEval = 'data-url';
filename = `${filename}`;
} else {
doEval = false;
if (isURL(filename)) {

View File

@@ -0,0 +1,74 @@
import { spawnPromisified } from '../common/index.mjs';
import * as fixtures from '../common/fixtures.js';
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
const importMetaMainScript = `
import assert from 'node:assert/strict';
assert.strictEqual(import.meta.main, true, 'import.meta.main should evaluate true in main module');
const { isMain: importedModuleIsMain } = await import(
${JSON.stringify(fixtures.fileURL('es-modules/import-meta-main.mjs'))}
);
assert.strictEqual(importedModuleIsMain, false, 'import.meta.main should evaluate false in imported module');
`;
function wrapScriptInEvalWorker(script) {
return `
import { Worker } from 'node:worker_threads';
new Worker(${JSON.stringify(script)}, { eval: true });
`;
}
function convertScriptSourceToDataUrl(script) {
return new URL(`data:text/javascript,${encodeURIComponent(script)}`);
}
function wrapScriptInUrlWorker(script) {
return `
import { Worker } from 'node:worker_threads';
new Worker(new URL(${JSON.stringify(convertScriptSourceToDataUrl(script))}));
`;
}
describe('import.meta.main in evaluated scripts', () => {
it('should evaluate true in evaluated script', async () => {
const result = await spawnPromisified(
process.execPath,
['--input-type=module', '--eval', importMetaMainScript],
);
assert.deepStrictEqual(result, {
stderr: '',
stdout: '',
code: 0,
signal: null,
});
});
it('should evaluate true in worker instantiated with module source by evaluated script', async () => {
const result = await spawnPromisified(
process.execPath,
['--input-type=module', '--eval', wrapScriptInEvalWorker(importMetaMainScript)],
);
assert.deepStrictEqual(result, {
stderr: '',
stdout: '',
code: 0,
signal: null,
});
});
it('should evaluate true in worker instantiated with `data:` URL by evaluated script', async () => {
const result = await spawnPromisified(
process.execPath,
['--input-type=module', '--eval', wrapScriptInUrlWorker(importMetaMainScript)],
);
assert.deepStrictEqual(result, {
stderr: '',
stdout: '',
code: 0,
signal: null,
});
});
});

View File

@@ -0,0 +1,26 @@
import '../common/index.mjs';
import assert from 'node:assert/strict';
import { Worker } from 'node:worker_threads';
function get_environment() {
if (process.env.HAS_STARTED_WORKER) return 'in worker thread started by ES Module';
return 'in ES Module';
}
assert.strictEqual(
import.meta.main,
true,
`\`import.meta.main\` at top-level module ${get_environment()} should evaluate \`true\``
);
const { isMain: importedModuleIsMain } = await import('../fixtures/es-modules/import-meta-main.mjs');
assert.strictEqual(
importedModuleIsMain,
false,
`\`import.meta.main\` at dynamically imported module ${get_environment()} should evaluate \`false\``
);
if (!process.env.HAS_STARTED_WORKER) {
process.env.HAS_STARTED_WORKER = 1;
new Worker(import.meta.filename);
}

View File

@@ -3,7 +3,7 @@ import assert from 'assert';
assert.strictEqual(Object.getPrototypeOf(import.meta), null);
const keys = ['dirname', 'filename', 'resolve', 'url'];
const keys = ['dirname', 'filename', 'main', 'resolve', 'url'];
assert.deepStrictEqual(Reflect.ownKeys(import.meta), keys);
const descriptors = Object.getOwnPropertyDescriptors(import.meta);

View File

@@ -0,0 +1 @@
export const isMain = import.meta.main;