Skip to content

esm: implement import.meta.main #57804

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

Open
wants to merge 45 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
e59c214
esm: implement import.meta.main
Lordfirespeed Apr 9, 2025
53fc3b0
fix: `require.main === module` is still accurate, remove todo
Lordfirespeed Apr 9, 2025
4ab6d86
fix: remove copy-paste artifact
Lordfirespeed Apr 9, 2025
10b0626
improve example usage of `import.meta.main`
Lordfirespeed Apr 9, 2025
151ca43
style: `if` blocks should be wrapped in braces
Lordfirespeed Apr 9, 2025
0154d53
style: arrange values alphabetically
Lordfirespeed Apr 9, 2025
df72c4c
fix: document correct stability, remove todo
Lordfirespeed Apr 9, 2025
642db17
style: assign `import.meta` members in alphabetical order
Lordfirespeed Apr 10, 2025
7224e88
move assignment of `module.isMain` to `compileSourceTextModule`
Lordfirespeed Apr 10, 2025
c31fc3e
fix: copy-paste artefact
Lordfirespeed Apr 10, 2025
fd7d355
fix: pass `context` parameter from translator function
Lordfirespeed Apr 14, 2025
3d3c3cc
Fix: missing semicolon
Lordfirespeed Apr 14, 2025
cc753b9
fix: failing test in folder with unusual characters
Lordfirespeed Apr 17, 2025
43f3121
remove todo; various reviewers had no qualms
Lordfirespeed Apr 17, 2025
3046280
prefer `kEmptyObject` to `{}`
Lordfirespeed Apr 20, 2025
31e1c93
add test for worker threads (passing)
Lordfirespeed Apr 20, 2025
c1223c2
improve assertion messages
Lordfirespeed Apr 20, 2025
cff69a1
fix: import `kEmptyObject`
Lordfirespeed Apr 20, 2025
22c4bc6
fix: remove newline + adhere to linting rules
Lordfirespeed Apr 20, 2025
36318dd
add `--eval` tests for `import.meta.main`
Lordfirespeed Apr 20, 2025
a050fa7
style: fix linter complaints
Lordfirespeed Apr 20, 2025
9926876
docs: add side-effect to `main()`
Lordfirespeed Apr 20, 2025
4ccf6eb
fix: assign `import.meta.main` to `true` for `--eval`
Lordfirespeed Apr 20, 2025
61c0d9e
style: resolve linter complaints
Lordfirespeed Apr 21, 2025
0dcf2a0
prefer `spawnPromisified` to `exec` with shell escaping
Lordfirespeed Apr 21, 2025
a6a9614
simplify test using top-level await
Lordfirespeed Apr 21, 2025
420c585
style: resolve linter complaints
Lordfirespeed Apr 21, 2025
b9836f6
commit @aduh95's suggestions r.e. test refactor-ability
Lordfirespeed Apr 21, 2025
46cfa4b
fix: accidental directory import is not permitted
Lordfirespeed Apr 21, 2025
45c3dfd
prefer `deepStrictEqual` to consecutive `strictEqual`
Lordfirespeed Apr 22, 2025
0a05adc
add `import.meta.main` test for worker with url input
Lordfirespeed Apr 22, 2025
28cb8fb
style: adjust test function names
Lordfirespeed Apr 22, 2025
48125ac
fix: run worker with `--expose-internals` and adjust url 'wrapper'
Lordfirespeed Apr 23, 2025
16e8bb6
avoid lookup in prototype by calling directly
Lordfirespeed Apr 24, 2025
0794ffa
docs: `*` bullets are reserved for signature annotations; convert off…
Lordfirespeed Apr 29, 2025
250573a
refactor: use functions to wrap scripts for each scenario
Lordfirespeed May 8, 2025
92e1993
refactor: extract `convertScriptSourceToDataUrl` function
Lordfirespeed May 8, 2025
4fe6f22
use a different strategy for running `data:` URL workers
Lordfirespeed May 8, 2025
616963b
prefer `runEntryPointWithESMLoader` to `getSourceSync` -> `stringify`…
Lordfirespeed May 8, 2025
98c3cfb
remove unused import
Lordfirespeed May 8, 2025
dc8d4f9
revert changes to `options.execArgv` handling
Lordfirespeed May 14, 2025
f0c6abc
inline `execOptions`
Lordfirespeed May 8, 2025
0bf97dd
prefer `JSON.stringify` to literal doublequotes
Lordfirespeed May 8, 2025
e065731
refactor: rewrite using `node:test` to ensure failure is caught in ca…
Lordfirespeed May 8, 2025
41d80f0
style: resolve linter complaints
Lordfirespeed May 14, 2025
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
34 changes: 34 additions & 0 deletions doc/api/esm.md
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,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
Expand Down Expand Up @@ -612,6 +641,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)`.
Expand Down Expand Up @@ -1172,6 +1205,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
Expand Down
14 changes: 14 additions & 0 deletions lib/internal/main/worker_thread.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 4 additions & 2 deletions lib/internal/modules/esm/initialize_import_meta.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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);
}
Expand Down
8 changes: 5 additions & 3 deletions lib/internal/modules/esm/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -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);
}

Expand Down
3 changes: 2 additions & 1 deletion lib/internal/modules/esm/translators.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});

Expand Down
12 changes: 10 additions & 2 deletions lib/internal/modules/esm/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const {
const {
emitExperimentalWarning,
getCWDURL,
kEmptyObject,
} = require('internal/util');
const assert = require('internal/assert');
const {
Expand Down Expand Up @@ -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 });
}

/**
Expand Down Expand Up @@ -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);
Expand Down
5 changes: 2 additions & 3 deletions lib/internal/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ const {
AtomicsAdd,
Float64Array,
FunctionPrototypeBind,
JSONStringify,
MathMax,
ObjectEntries,
Promise,
Expand Down Expand Up @@ -166,8 +165,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)) {
Expand Down
74 changes: 74 additions & 0 deletions test/es-module/test-esm-import-meta-main-eval.mjs
Original file line number Diff line number Diff line change
@@ -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,
});
});
});
26 changes: 26 additions & 0 deletions test/es-module/test-esm-import-meta-main.mjs
Original file line number Diff line number Diff line change
@@ -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);
}
2 changes: 1 addition & 1 deletion test/es-module/test-esm-import-meta.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions test/fixtures/es-modules/import-meta-main.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const isMain = import.meta.main;