Skip to content

Commit 430e66b

Browse files
Lordfirespeedaduh95
andcommitted
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 <[email protected]> PR-URL: #57804 Backport-PR-URL: #58693 Fixes: #57226 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Guy Bedford <[email protected]> Reviewed-By: Antoine du Hamel <[email protected]> Reviewed-By: Marco Ippolito <[email protected]>
1 parent f99aa74 commit 430e66b

File tree

11 files changed

+174
-12
lines changed

11 files changed

+174
-12
lines changed

doc/api/esm.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,35 @@ import { readFileSync } from 'node:fs';
393393
const buffer = readFileSync(new URL('./data.proto', import.meta.url));
394394
```
395395
396+
### `import.meta.main`
397+
398+
<!-- YAML
399+
added:
400+
- REPLACEME
401+
-->
402+
403+
> Stability: 1.0 - Early development
404+
405+
* {boolean} `true` when the current module is the entry point of the current process; `false` otherwise.
406+
407+
Equivalent to `require.main === module` in CommonJS.
408+
409+
Analogous to Python's `__name__ == "__main__"`.
410+
411+
```js
412+
export function foo() {
413+
return 'Hello, world';
414+
}
415+
416+
function main() {
417+
const message = foo();
418+
console.log(message);
419+
}
420+
421+
if (import.meta.main) main();
422+
// `foo` can be imported from another module without possible side-effects from `main`
423+
```
424+
396425
### `import.meta.resolve(specifier)`
397426
398427
<!-- YAML
@@ -587,6 +616,10 @@ These CommonJS variables are not available in ES modules.
587616
They can instead be loaded with [`module.createRequire()`][] or
588617
[`process.dlopen`][].
589618
619+
#### No `require.main`
620+
621+
To replace `require.main === module`, there is the [`import.meta.main`][] API.
622+
590623
#### No `require.resolve`
591624
592625
Relative resolution can be handled via `new URL('./local', import.meta.url)`.
@@ -1110,6 +1143,7 @@ resolution for ESM specifiers is [commonjs-extension-resolution-loader][].
11101143
[`import()`]: #import-expressions
11111144
[`import.meta.dirname`]: #importmetadirname
11121145
[`import.meta.filename`]: #importmetafilename
1146+
[`import.meta.main`]: #importmetamain
11131147
[`import.meta.resolve`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import.meta/resolve
11141148
[`import.meta.url`]: #importmetaurl
11151149
[`import`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import

lib/internal/main/worker_thread.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,20 @@ port.on('message', (message) => {
203203
break;
204204
}
205205

206+
case 'data-url': {
207+
const { runEntryPointWithESMLoader } = require('internal/modules/run_main');
208+
209+
RegExpPrototypeExec(/^/, ''); // Necessary to reset RegExp statics before user code runs.
210+
const promise = runEntryPointWithESMLoader((cascadedLoader) => {
211+
return cascadedLoader.import(filename, undefined, { __proto__: null }, true);
212+
});
213+
214+
PromisePrototypeThen(promise, undefined, (e) => {
215+
workerOnGlobalUncaughtException(e, true);
216+
});
217+
break;
218+
}
219+
206220
default: {
207221
// script filename
208222
// runMain here might be monkey-patched by users in --require.

lib/internal/modules/esm/initialize_import_meta.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,12 @@ function createImportMetaResolve(defaultParentURL, loader, allowParentURL) {
5151
/**
5252
* Create the `import.meta` object for a module.
5353
* @param {object} meta
54-
* @param {{url: string}} context
54+
* @param {{url: string, isMain?: boolean}} context
5555
* @param {typeof import('./loader.js').ModuleLoader} loader Reference to the current module loader
5656
* @returns {{dirname?: string, filename?: string, url: string, resolve?: Function}}
5757
*/
5858
function initializeImportMeta(meta, context, loader) {
59-
const { url } = context;
59+
const { url, isMain } = context;
6060

6161
// Alphabetical
6262
if (StringPrototypeStartsWith(url, 'file:') === true) {
@@ -65,6 +65,8 @@ function initializeImportMeta(meta, context, loader) {
6565
setLazyPathHelpers(meta, url);
6666
}
6767

68+
meta.main = !!isMain;
69+
6870
if (!loader || loader.allowImportMetaResolve) {
6971
meta.resolve = createImportMetaResolve(url, loader, experimentalImportMetaResolve);
7072
}

lib/internal/modules/esm/loader.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -246,10 +246,11 @@ class ModuleLoader {
246246
*
247247
* @param {string} source Source code of the module.
248248
* @param {string} url URL of the module.
249+
* @param {{ isMain?: boolean }|undefined} context - context object containing module metadata.
249250
* @returns {object} The module wrap object.
250251
*/
251-
createModuleWrap(source, url) {
252-
return compileSourceTextModule(url, source, this);
252+
createModuleWrap(source, url, context = kEmptyObject) {
253+
return compileSourceTextModule(url, source, this, context);
253254
}
254255

255256
/**
@@ -288,7 +289,8 @@ class ModuleLoader {
288289
* @returns {Promise<object>} The module object.
289290
*/
290291
eval(source, url, isEntryPoint = false) {
291-
const wrap = this.createModuleWrap(source, url);
292+
const context = isEntryPoint ? { isMain: true } : undefined;
293+
const wrap = this.createModuleWrap(source, url, context);
292294
return this.executeModuleJob(url, wrap, isEntryPoint);
293295
}
294296

@@ -636,6 +638,7 @@ class ModuleLoader {
636638
* @param {string} parentURL Path of the parent importing the module.
637639
* @param {Record<string, string>} importAttributes Validations for the
638640
* module import.
641+
* @param {boolean} [isEntryPoint] Whether this is the realm-level entry point.
639642
* @returns {Promise<ModuleExports>}
640643
*/
641644
async import(specifier, parentURL, importAttributes, isEntryPoint = false) {

lib/internal/modules/esm/translators.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,8 @@ translators.set('module', function moduleStrategy(url, source, isMain) {
103103
source = stringify(source);
104104
debug(`Translating StandardModule ${url}`);
105105
const { compileSourceTextModule } = require('internal/modules/esm/utils');
106-
const module = compileSourceTextModule(url, source, this);
106+
const context = isMain ? { isMain } : undefined;
107+
const module = compileSourceTextModule(url, source, this, context);
107108
return module;
108109
});
109110

lib/internal/modules/esm/utils.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ const {
3838
const {
3939
emitExperimentalWarning,
4040
getCWDURL,
41+
kEmptyObject,
4142
} = require('internal/util');
4243
const {
4344
setImportModuleDynamicallyCallback,
@@ -187,7 +188,7 @@ function registerModule(referrer, registry) {
187188
*/
188189
function defaultInitializeImportMetaForModule(meta, wrap) {
189190
const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
190-
return cascadedLoader.importMetaInitialize(meta, { url: wrap.url });
191+
return cascadedLoader.importMetaInitialize(meta, { url: wrap.url, isMain: wrap.isMain });
191192
}
192193

193194
/**
@@ -337,15 +338,22 @@ async function initializeHooks() {
337338
* @param {string} source Source code of the module.
338339
* @param {typeof import('./loader.js').ModuleLoader|undefined} cascadedLoader If provided,
339340
* register the module for default handling.
341+
* @param {{ isMain?: boolean }|undefined} context - context object containing module metadata.
340342
* @returns {ModuleWrap}
341343
*/
342-
function compileSourceTextModule(url, source, cascadedLoader) {
344+
function compileSourceTextModule(url, source, cascadedLoader, context = kEmptyObject) {
343345
const hostDefinedOption = cascadedLoader ? source_text_module_default_hdo : undefined;
344346
const wrap = new ModuleWrap(url, undefined, source, 0, 0, hostDefinedOption);
345347

346348
if (!cascadedLoader) {
347349
return wrap;
348350
}
351+
352+
const { isMain } = context;
353+
if (isMain) {
354+
wrap.isMain = true;
355+
}
356+
349357
// Cache the source map for the module if present.
350358
if (wrap.sourceMapURL) {
351359
maybeCacheSourceMap(url, source, wrap, false, wrap.sourceURL, wrap.sourceMapURL);

lib/internal/worker.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ const {
77
AtomicsAdd,
88
Float64Array,
99
FunctionPrototypeBind,
10-
JSONStringify,
1110
MathMax,
1211
ObjectEntries,
1312
Promise,
@@ -166,8 +165,8 @@ class Worker extends EventEmitter {
166165
doEval = 'classic';
167166
} else if (isURL(filename) && filename.protocol === 'data:') {
168167
url = null;
169-
doEval = 'module';
170-
filename = `import ${JSONStringify(`${filename}`)}`;
168+
doEval = 'data-url';
169+
filename = `${filename}`;
171170
} else {
172171
doEval = false;
173172
if (isURL(filename)) {
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { spawnPromisified } from '../common/index.mjs';
2+
import * as fixtures from '../common/fixtures.js';
3+
import assert from 'node:assert/strict';
4+
import { describe, it } from 'node:test';
5+
6+
const importMetaMainScript = `
7+
import assert from 'node:assert/strict';
8+
9+
assert.strictEqual(import.meta.main, true, 'import.meta.main should evaluate true in main module');
10+
11+
const { isMain: importedModuleIsMain } = await import(
12+
${JSON.stringify(fixtures.fileURL('es-modules/import-meta-main.mjs'))}
13+
);
14+
assert.strictEqual(importedModuleIsMain, false, 'import.meta.main should evaluate false in imported module');
15+
`;
16+
17+
function wrapScriptInEvalWorker(script) {
18+
return `
19+
import { Worker } from 'node:worker_threads';
20+
new Worker(${JSON.stringify(script)}, { eval: true });
21+
`;
22+
}
23+
24+
function convertScriptSourceToDataUrl(script) {
25+
return new URL(`data:text/javascript,${encodeURIComponent(script)}`);
26+
}
27+
28+
function wrapScriptInUrlWorker(script) {
29+
return `
30+
import { Worker } from 'node:worker_threads';
31+
new Worker(new URL(${JSON.stringify(convertScriptSourceToDataUrl(script))}));
32+
`;
33+
}
34+
35+
describe('import.meta.main in evaluated scripts', () => {
36+
it('should evaluate true in evaluated script', async () => {
37+
const result = await spawnPromisified(
38+
process.execPath,
39+
['--input-type=module', '--eval', importMetaMainScript],
40+
);
41+
assert.deepStrictEqual(result, {
42+
stderr: '',
43+
stdout: '',
44+
code: 0,
45+
signal: null,
46+
});
47+
});
48+
49+
it('should evaluate true in worker instantiated with module source by evaluated script', async () => {
50+
const result = await spawnPromisified(
51+
process.execPath,
52+
['--input-type=module', '--eval', wrapScriptInEvalWorker(importMetaMainScript)],
53+
);
54+
assert.deepStrictEqual(result, {
55+
stderr: '',
56+
stdout: '',
57+
code: 0,
58+
signal: null,
59+
});
60+
});
61+
62+
it('should evaluate true in worker instantiated with `data:` URL by evaluated script', async () => {
63+
const result = await spawnPromisified(
64+
process.execPath,
65+
['--input-type=module', '--eval', wrapScriptInUrlWorker(importMetaMainScript)],
66+
);
67+
assert.deepStrictEqual(result, {
68+
stderr: '',
69+
stdout: '',
70+
code: 0,
71+
signal: null,
72+
});
73+
});
74+
});
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import '../common/index.mjs';
2+
import assert from 'node:assert/strict';
3+
import { Worker } from 'node:worker_threads';
4+
5+
function get_environment() {
6+
if (process.env.HAS_STARTED_WORKER) return 'in worker thread started by ES Module';
7+
return 'in ES Module';
8+
}
9+
10+
assert.strictEqual(
11+
import.meta.main,
12+
true,
13+
`\`import.meta.main\` at top-level module ${get_environment()} should evaluate \`true\``
14+
);
15+
16+
const { isMain: importedModuleIsMain } = await import('../fixtures/es-modules/import-meta-main.mjs');
17+
assert.strictEqual(
18+
importedModuleIsMain,
19+
false,
20+
`\`import.meta.main\` at dynamically imported module ${get_environment()} should evaluate \`false\``
21+
);
22+
23+
if (!process.env.HAS_STARTED_WORKER) {
24+
process.env.HAS_STARTED_WORKER = 1;
25+
new Worker(import.meta.filename);
26+
}

test/es-module/test-esm-import-meta.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import assert from 'assert';
33

44
assert.strictEqual(Object.getPrototypeOf(import.meta), null);
55

6-
const keys = ['dirname', 'filename', 'resolve', 'url'];
6+
const keys = ['dirname', 'filename', 'main', 'resolve', 'url'];
77
assert.deepStrictEqual(Reflect.ownKeys(import.meta), keys);
88

99
const descriptors = Object.getOwnPropertyDescriptors(import.meta);

0 commit comments

Comments
 (0)