Open MylesBorins opened 5 years ago
@devsnek for WASI, it is just the "main"
string.
@bmeck right, i mean the semantics of handling that main.
We might also want to lock step with WASM interface-types so that WASM side doesn't go out of sync with ESM
A complication is that there's more questions that may potentially need answering than "is this file the entry point" - there's also "is this file a loader entry point", "is this file being consumed by a loader entry point", "is this file used in --require", "is this file being consumed within --require", "was this file eval'd", etc. require.main
doesn't answer all these questions either :-/
@ljharb I'd agree that question isn't solved here; if there is a more universal solution we might want to look at other things. I think adding conditions for things like --require
style usage would be nice but likely is a different topic and might be better served with a different mechanism than how an entry point is bootstrapped. Even if we call main()
it doesn't mean that the file is the only entrypoint, and userland could always call main()
on other modules manually.
In case others find it useful, es-main
is a package that allows for a check similar to require.main === module
.
import esMain from 'es-main';
if (esMain(import.meta)) {
// Do something special.
}
This works for node script.js
, node script
, and ./script.js
type use.
@tschaub that would appear to have some bugs involving process.argv[1] not being present (repl) and returning true when i assume you would want it to return false (argv[1] is a
, a.x
is loaded, you call that from a.y
and it returns true).
@devsnek - thanks for the report. Pull requests accepted :)
I have opened a PR to implement the feature on node repo, does anyone want to raise their voice regarding the value you would expect import.meta.main
to take on a Worker
thread?
From https://github.com/nodejs/node/pull/32223#issuecomment-598355387
What would the value be in a Worker thread?
Good question! With the current implementation, it is
true
for the Worker entry point, andfalse
for sub-modules. To check if the current script is run as CLI, that means you also have to check forisMainThread
value... I'm not sure if that's the behaviour we want to implement, but it definitely needs documentation of this topic.
FWIW Deno follows the same logic.
Considering a package like es-main
exists (and can be improved for edge cases), maybe we can close this issue? Or is it still worth having it built-in?
es-main
is working great for me in replacement of require.main === module
.
+1 as contributor to jslint this feature would be useful. latest version of jslint doesn't use npm for installation.
ideally, i would like jslint to be a tool-less, self-contained file able to run both from cli/esm.
#!/bin/sh
# install tool-less jslint.mjs by simply downloading self-contained file
curl https://www.jslint.com/jslint.js > jslint.mjs
# run jslint.mjs from cli
node jslint.mjs foo.js // jslint foo.js
# use jslint.mjs as imported esm
node --input-type=module -e "////'"'
import jslint from "./jslint.mjs"
let code = "..."
let result = jslint(code);
result.warnings.forEach(function (warning) {
console.error(warning);
});
'
FYI, there is a, similar but not same, magic comment /*@__PURE__*/
in bundler, Supported by rollup, esbuild, etc.
Which allow you to remove command line only code from output.
// library code
/* @__PURE__ */(async () => {
// command line only code
})();
Work both in nowadays Node.js and bundler, such as rollup, esbuild.
TBH, very ugly workaround. 🙁
// library code
/*@__PURE__*/(async () => {
// command line only code
if ((await import('es-main')).default(import.meta)) {
// command line only main code
}
})()
I can't believe it's taking longer than 2 years and 63 comments to decide how to add one boolean that a lot of users want and that is trivial to implement.
I'm having the impression that this feature is not getting implemented because some node developers "dislike the idea of differentiating entrypoints" and want to force us to "use a separate file for your bin".
Do we really need to complicate our lives to use import.meta
because some power tripping mod wants to dictate how we are supposed to code?!
Doing That can be handled by the bundler instead.process.argv[1] === __filename
has the advantage of being compatible with both cjs and esm.
@millsp ESM doesn't have __filename
.
I read all the comments but I can't understand why this issue is still open after 3 years but it's not going anywhere.
Are you still stuck deciding whether you want this feature at all, or what else is blocking this? After longer than three years and a hundred comments I think it's time to decide: please close the issue if you really don't want it.
Otherwise, if you are still really undecided, why don't you add something like a import.meta.nodeJsExperimentalMain
property?
This is why I would love to have this feature, even experimentally.
When I quickly jot down some prototype, I like to include in some modules something to quickly test them. A bunch of asserts and printing out some values. It's code that I'm writing quickly to see how a certain library or design feels like. Most of the times I end up discarding this code, or heavily refactoring it: the module's API is almost surely guaranteed to change.
Having some dedicated unit tests or extra files for this stuff feels way too much, to the point that I'm using CommonJS instead of ES modules only because of this... I would love to use import.meta.nodeJsExperimentalMain
for this. Then, once my code is a bit more mature and I start refactoring it, I'd set up mocha and move this testing code into proper tests.
Our use case: We have a bin file that we do not want to allow it to be imported by other scripts, only run on the command line However we allow one exception to the above. We allow it to be imported for unit testing so we can mock certain external calls.
If there is another way to solve this I would love to hear it.
if (require.main === module) {
(async () => {
try {
const program = getProgram();
program.parse(process.argv);
console.log('Starting Scan');
await startScan(program);
console.log('Scan completed successfully');
} catch (e) {
console.error(`Scan failed: ${e}`);
process.exit(1);
}
})();
}
module.exports = {
getProgram,
startScan,
};
@rmclaughlin-nelnet I believe Object.keys(require.cache).length === 1
will be true if it's the first file being evaluated.
Sorry I should have been more clear. We are trying to upgrade this module to ESM. So the require object is not available. My question is how do we duplicate this functionality without the new feature mentioned above.
import url from "url";
if (import.meta.url === url.pathToFileURL(process.argv[1]).href) {
// module was not imported but called directly
}
import url from "url"; if (import.meta.url === url.pathToFileURL(process.argv[1]).href) { // module was not imported but called directly }
Unfortunately that wont work for us because we call our scripts without the extension https://github.com/nodejs/node/issues/49440
The es-main
package accounts for this and other nuances. See https://github.com/nodejs/node/issues/49440
require.module === main
is particularly useful when writing and testing Node GitHub Actions, although I could make this same argument for any CLI. The entrypoint file for a Node action is defined in the action.yml
file, and when that file is executed it must perform the action. A file that only calls another function is cumbersome, especially when you have dozens of these actions.
Some examples:
require.module === main
entrypoint.js
const core = require('@actions/core');
module.exports = function run(deps) { }
if (require.main === module) {
run({ core });
}
entrypoint.test.js
const test = require('node:test');
const assert = require('node:assert');
const sinon = require('sinon');
const run = require('./entrypoint');
test('run', (t) => {
run({ core: sinon.stub() });
assert(true);
});
run.js
module.exports = function run(deps) { }
entrypoint.js
const core = require('@actions/core');
const run = require('./run');
run({ core });
entrypoint.test.js
const test = require('node:test');
const assert = require('node:assert');
const sinon = require('sinon');
const run = require('./run');
test('run', (t) => {
run({ core: sinon.stub() });
assert(true);
});
I much prefer the require.main === module
solution, for the following reasons:
entrypoint.js
files are repetitive and often identical.Simple workaround with loader
:
entrypoint.js
if (import.meta.main ) {
// start
}
mainLoader.js
import {pathToFileURL} from 'node:url';
import {argv} from 'node:process';
import {Buffer} from 'node:buffer';
const mainUrl = pathToFileURL(argv[1]).href;
export const load = async (specifier, context, nextLoad) => {
if (specifier === mainUrl) {
let {source, ...rest} = await nextLoad(specifier, context, nextLoad);
if (Buffer.isBuffer(source)) {
source = Buffer.concat([Buffer.from('import.meta.main = true;'),source]);
return {source, ...rest};
}
}
return nextLoad(specifier, context, nextLoad);
};
node --loader=mainLoader.js entrypoint.js
I use (import.meta.url === ('file:///'+process.argv[1].replace(/\\/g,'/')).replace(/\/{3,}/,'///'))
work in node 18+
https://github.com/nodejs/node/issues/49440
This works for
node script.js
,node script
, and./script.js
type use.
Unfortunately if we are having bin/index.js
this doesn't work for node bin
but only node bin/index.js
, node bin/index
, and ./bin/index.js
.
so we have to change the detecting method to
import {createRequire} from 'module';
import {fileURLToPath} from 'url';
if (process.argv[1] && createRequire(process.argv[1]).resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
// main();
}
// or
const esMain = meta => ((process.argv[1] && createRequire(process.argv[1]).resolve(process.argv[1])) === fileURLToPath(meta?.url));
if (esMain(import.meta)) {
// main();
}
@lwr - Is your comment above related to the es-main
package? I wasn't sure what you were quoting with "This works for ...".
@tschaub yes, it is about what es-main did not implements
@lwr - The es-main tests cover this case (running node test/resolve-index
and asserting that esMain(import.meta)
returns true
in test/resolve-index/index.js
). But it sounds like you are running into an issue. If you can open a ticket here, we can discuss the details: https://github.com/tschaub/es-main/issues
@tschaub ok, I saw it is fixed in https://github.com/tschaub/es-main/pull/28
For me it would be useful if one had access to a URL of the executed script, in CJS provided by require.main.filename
. With such a property, the above mentioned comparison could just be a hypothetical import.meta.url === import.meta.mainUrl
. This is much more useful than that boolean that deno has.
it would be useful if one had access to a URL of the executed script
Same here, the es-main
module doesn't help us. We are not concerned if the current script is the entry script. We need the path/url to the entry script.
With cjs we can just use require.main.filename
.
With esm, it sounds like our only option is to find the entry point in process.argv
?
It's worth noting that I don't see any reason why this should be on import.meta
. It could go in utils
or anywhere else.
It could go in
utils
or anywhere else.
The consensus is node:
exports should remain stable from one module to the next, i.e. if you import { isMain } from 'node:util'
, it should always be either false
or true
for all the modules, and not change depending on which module imported it. If this is making its way into Node.js, my guess is that putting it on import.meta
is where it's the least likely to be controversial.
With esm, it sounds like our only option is to find the entry point in
process.argv
?
What's wrong with that? process.argv[1]
contains the full path to the entry script in all Node.js versions, and works in both CommonJS and ESM.
What's wrong with that?
process.argv[1]
This makes it stiffer for use with Node. process
does not exist in the browser environment or Deno.
Using process.argv
along with a name other than "main" also has the advantage that there'd be no confusion about what "main" means - this feature would just be for the CLI entrypoint.
Since Node 20.11.0:
if ( import.meta.filename === process?.argv[1] ) {
// Node main script
}
if ( import.meta.filename === process?.argv[1] ) { // Node main script }
This only works for a limited set of cases. For example, if you have an example.js
script with the following content:
// example.js
console.log('main?', import.meta.filename === process?.argv[1]);
This prints main? true
when the script is invoked with something like node example.js
.
However, it prints main? false
when the script is invoked with node example
.
If you were to create a symbolic link with ln -s example.js link.js
, then node link.js
also prints main? false
.
If you try to use this same logic in a "bin" script, executing it with npm exec example-bin
will also print main? false
.
Ideally, import.meta.main
would be present in Node (as it is in Deno and Bun). Until then, the es-main
package handles these cases.
It would be interesting to hear which of the original objectors to import.meta.main
are still objecting now that the definition is clearly "CLI entry point only" (not workers / other top-level imports) and it's implemented successfully in other platforms. The CLI use case is still important for Node.js and just like we have worked to make this easier with automatic format detection, this is very much another remaining historical friction point, where perhaps the original contentions have shifted by now.
import.meta.filename
is a nice feature. Means we don't have to do the fileURLToPath
dance anymore! Profit! (EDIT: Whoa, there's also import.meta.dirname
? Sick!)
But, if the use case is "CLI entry point" only, what if we went back to basics?
my-cli-app.js
// import this and that...
export default function main (...argv) {
// ...CLI entrypoint code..
}
// ...the rest of your app...
Running this with node my-cli-app.js yeet yoink
would call main("yeet", "yoink")
, and that's that.[^0] Elegant as fuck!
It even uses nice modern spread syntax to prevent people from overdoing things and bolting more incompatible crap onto the entrypoint. Essential!
Furthermore, being able to import an app's main CLI module could (re-)enable composability of command-line tools (that have a compliant default main
), without inducing tradeoffs (such as "spawn additional process(es)" vs. "learn additional scripting APIs"). Sanity-preserving!
(Signed: a guy who still has to use a CommonJS kludge to launch his Node CLI apps that are otherwise ESM all the way down.)
[^0]: Just to spell it out: no it would not have to be named "main"; yes it would also be able to be an arrow function; yes it would also be able to be async; yes it would receive all arguments as strings (isn't that the original reason behind the "weird" type coercions in JS?)
If you're in a CJS module and the entrypoint was ESM, neither require.main.filename
nor import.meta.main
would work to get the path to the CLI entrypoint. Would it make more sense to expose this as something like process.cliEntrypoint
or process.cliMain
so that the same API can be used everywhere?
Since Node 20.11.0:
if ( import.meta.filename === process?.argv[1] ) { // Node main script }
Another limitation of this is it would return false positive if the same module is loaded twice (e.g. entry point is file:///module.js
and file:///module.js?notEntryPoint
is imported later).
Would it make more sense to expose this as something like
process.cliEntrypoint
orprocess.cliMain
so that the same API can be used everywhere?
What would be the value of process.cliEntryPoint
? How do you envision using it?
:cricket: :cricket: :cricket:
So what's wrong with using the default
export of the file passed to node
as the entrypoint?
@egasimus you can probably find tons of modules that have been written with a default export which they don't expect to be run when module is the entrypoint. https://github.com/nodejs/node/pull/32223#issuecomment-703810755 suggested using a named main
export for that, which seems less dangerous than using the default
one. In any case, it is not going to happen until someone opens a PR implementing it.
What would be the value of
process.cliEntryPoint
? How do you envision using it?
The value would be the same as import.meta.main
as proposed here, plus the ability to access it consistently in any module whether it's CJS or ESM.
What would be the value of
process.cliEntryPoint
? How do you envision using it?The value would be the same as
import.meta.main
as proposed here, plus the ability to access it consistently in any module whether it's CJS or ESM.
The glaring difference is that import.meta
is module-specific (each module receives a different import.meta
object), while a global object such as process
is the same for all modules (and non-modules) on the same realm, so we couldn't pass a boolean value there.
I see, I misunderstood the intended meaning of import.meta.main
since there was a considerable amount of discussion of process.argv[1]
. I'll open a separate issue (https://github.com/nodejs/node/issues/51840).
Welcome in 2024
Deno just added this.
Should we?
https://github.com/denoland/deno/pull/1835