Skip to content
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

import.meta.main #49440

Open
MylesBorins opened this issue Feb 26, 2019 · 99 comments · May be fixed by #32223
Open

import.meta.main #49440

MylesBorins opened this issue Feb 26, 2019 · 99 comments · May be fixed by #32223
Labels
esm Issues and PRs related to the ECMAScript Modules implementation. feature request Issues that request new features to be added to Node.js.

Comments

@MylesBorins
Copy link
Contributor

Deno just added this.

Should we?

denoland/deno#1835

@ljharb
Copy link
Member

ljharb commented Feb 26, 2019

I'm a bit confused what it does - does it mean "it was the main in package.json"? it was the primary entry point? what about import()?

@devsnek
Copy link
Member

devsnek commented Feb 26, 2019

it's true if the file was the entrypoint. for node i'd rather it be called something else cuz the term main already has package-scoped meaning.

@ljharb
Copy link
Member

ljharb commented Feb 26, 2019

Right, but anything that's import()ed is also an entry point, and I'm very unclear on why you care what that value is. Currently you can look at process.argv and __filename to determine that (and import.meta.filename or similar is quite well motivated).

Also yes, main is a terrible name for multiple reasons, not the least of which is that it's a package.json field.

@guybedford
Copy link
Contributor

I really like the import.meta.main concept. Currently in Node.js it is common for CLI tools to do the following check:

if (require.main === module) {
  console.log('This is a CLI tool! Usage: ...');
}

We currently have no version of the above check in ECMAScript modules, so this is a problem that comes up, and there is currently no easy way to do this without it feeling like a hack.

As an already-paved use case, users are familiar with the main terminology in this context already.

The above check then becomes:

if (import.meta.main) {
  console.log('This is a CLI tool! Usage: ...');
}

and the really nice thing about this pattern is that it works in browsers and other environments completely fine without any need for compatibility API layers or compilation.

For these reasons I'm 100% supportive of import.meta.main.

@ljharb
Copy link
Member

ljharb commented Feb 27, 2019

Doesn’t that use case predate the current community preference, which is to have separate packages for a library and a CLI?

@guybedford
Copy link
Contributor

@ljharb this use case is explicitly documented in https://github.com/nodejs/node/blob/master/doc/api/modules.md#accessing-the-main-module.

@ljharb
Copy link
Member

ljharb commented Feb 27, 2019

Sure but so is every aspect of the require algorithm :-) the better question is, is it still a common or desired use case to determine if something is the entry point, and how does that change now that dynamic import allows multiple entry points?

@guybedford
Copy link
Contributor

@ljharb yes it is a desired feature. The require.main === module was something we had to explicitly support in the ncc project as users were using it in CLI tools. This came up pretty early in the project and had multiple users asking about it here - vercel/ncc#224. Dynamic import does not affect this, as what we are distinguishing is the CLI entry point, not the module graph entry point.

@ljharb
Copy link
Member

ljharb commented Feb 27, 2019

The feature seems fine then (it’d only be true in the top-level process entry point); the name, definitely not (but we can bikeshed that).

@bmeck
Copy link
Member

bmeck commented Feb 27, 2019

Does this absolutely need to be in our first iteration or can we get feedback / focus on other things?

@zenparsing
Copy link

Thanks for bringing this up @MylesBorins. I (think) we can say this is post-MVP but it's good to have on our radar.

@MylesBorins
Copy link
Contributor Author

can definitely be post MVP. Just wanted to document what's going on in ecosystem

@shian15810
Copy link

shian15810 commented Aug 1, 2019

Well, I'm not part of the organization, but as a cli maintainer, this proposal seems so interesting.

Can I start to try implementing this in https://github.com/nodejs/node and possibly a PR now?

@devsnek
Copy link
Member

devsnek commented Aug 1, 2019

I'm not a huge fan of the concept of main. imo you should have separate files for bin and lib, which is something other languages do just fine, and installers like npm and yarn already let you specify separate bins.

@guybedford
Copy link
Contributor

@shian15810 thanks for showing an interest in the modules work! Contributions are very much welcome to Node.js core and the module work here.

We do have some consensus issues for this feature due to our not wanting to provide unnecessary features without seeing a strong need for them in the initial modules implementation. import.meta is a very widely used namespace (every module!) with strong backwards compatibility needs, so we do also need to be very cautious about what we put on it.

A PR to Node.js core would certainly drive discussion, and may even sway consensus. Also hearing more about your use case as a CLI maintainer and how this is useful could help us better understand the importance of the feature to you (including for example the points raised by Gus above). Even if the PR sits without approval, we may be able to come back to it in a few months even as well.

@shian15810
Copy link

shian15810 commented Aug 1, 2019

The use case of mine is that both bin and main fields in package.json point to the same file. Using require.main === module, as mentioned in the official docs here, to tell whether the script has been run directly or not, so that to know if the main function should be executed or exported.

As far as I can tell, with a package.json having "type": "commonjs", there are three ways to tell if a file is run directly (aka not being required):

  1. require.main === module or process.mainModule === module
  2. require.main.filename === __filename or process.mainModule.filename === __filename
  3. !module.parent

However, with a package.json having "type": "module", require and module are not available, as well as process.mainModule is undefined.

What's left is just import.meta.url, but the problem is what to compare with to know if it is the main script.

In addition, according to this, module.parent in a cjs script will be undefined if the parent is a mjs script. After some testing, require.main and process.mainModule are both undefined too in this context.

In my imagination, if import.meta.main is implemented, I could use it in mjs script, while retaining require.main === module in cjs script. But this still pose a serious problem to a project with mixed cjs and mjs scripts importing each other, as mentioned above.

So yeah, I agree with @devsnek in this case, that is pointing bin and main to separate scripts in the first place, which I now think is the right way to solve this problem.

Also, I agree with @guybedford regarding strong backwards compatibility, even though TC39 states that import.meta object will be extensible.

As a last note, I've read somewhere in this repo stating that in the context of esmodule, there is no concept of main such as in commonjs, correct me if I'm wrong. But I certainly think that there must be some other use cases of determining the main script, just like require.main === module in commonjs. It is good to have import.meta.main as the counterpart in esmodule, though should it be named main is pretty much debatable.

@MylesBorins
Copy link
Contributor Author

Going to abandon this for now, please feel free to reopen

@guybedford
Copy link
Contributor

I still believe this is an important feature, to be put in the same basket as CJS features not available in ESM like require.resolve.

It's not a common feature, but it is definitely a heavily ingrained one where it is used, and has a small collection of users that absolutely do expect this functionality (mainly CLI developers).

@devsnek
Copy link
Member

devsnek commented Nov 17, 2019

i am still against the concept of a main module in general.

@SMotaal
Copy link

SMotaal commented Nov 17, 2019

@guybedford Which labels are relevant here? Can you add them to make it easier to related to the context please — ie cjs anr/or esm... etc.

@SMotaal
Copy link

SMotaal commented Nov 17, 2019

@devsnek Can you elaborate more? Main here is the main entrypoint, so it is like window.location where there is a Window context or self.location where there is not (workers... etc.) but that does not apply the same way in CommonJS (ie not URLs).

Aside — I don't think adding a global for ESM only is a good idea personally, and otherwise neutral to import.meta.main.

@guybedford guybedford added the esm Issues and PRs related to the ECMAScript Modules implementation. label Nov 17, 2019
@SMotaal
Copy link

SMotaal commented Nov 17, 2019

@guybedford Awesome, that really helps 💯

@devsnek
Copy link
Member

devsnek commented Nov 17, 2019

@SMotaal i dislike the idea of differentiating entrypoints. you can just use a separate file for your bin.

@devsnek
Copy link
Member

devsnek commented Nov 17, 2019

being an entrypoint doesn't inherently mean it's a cli (for example, cf workers). additionally, some hosts like browsers don't map well to the concept of entrypoints. depending on how you think about it, a webpage may either have several or no entrypoints. Even if you choose to think about a browser having several instead of none, none of the several would be a cli.

@SMotaal
Copy link

SMotaal commented Nov 17, 2019

@SMotaal i dislike the idea of differentiating entrypoints. you can just use a separate file for your bin.

@devsnek certainly it is one style, I am not saying your style is wrong, but others also have justifications that cannot be open to judgement because this all comes down to matters of opinion.

As it happens, any given module in a package can be called as a result of at least two paths, a main entrypoint in the package, or an entrypoint for which the package is a direct or indirect dependency… some packages may actually consider this a functional parameter depending on their purpose.

depending on how you think about it

Yes, I think this is the bottom line… And, so no disagreement or disapproval is intended, just wanting us to consider the more diverse landscapes that we may not ourselves be interested in (yet).

@guybedford
Copy link
Contributor

To clarify some of the above, the only module that gets import.meta.main set is the module passed into node main.js it is only the module corresponding to process.argv[1]. And no, it doesn't apply to browsers.

This is the Node.js application entry point main, not package entry points, or realm / worker entry points.

An equivalent way to achieve this check would be to do path.resolve(process.argv[1]) === fileURLToPath(import.meta.url).

Perhaps the above explicit check would be enough though.

@tschaub
Copy link

tschaub commented Sep 2, 2023

@lwr - Is your comment above related to the es-main package? I wasn't sure what you were quoting with "This works for ...".

@lwr
Copy link

lwr commented Sep 2, 2023

@tschaub yes, it is about what es-main did not implements

@tschaub
Copy link

tschaub commented Sep 2, 2023

@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

@lwr
Copy link

lwr commented Sep 4, 2023

@tschaub ok, I saw it is fixed in tschaub/es-main#28

@silverwind
Copy link
Contributor

silverwind commented Dec 14, 2023

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.

@timfish
Copy link
Contributor

timfish commented Jan 4, 2024

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.

@aduh95
Copy link
Contributor

aduh95 commented Jan 4, 2024

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.

@targos
Copy link
Member

targos commented Jan 4, 2024

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.

@sosoba
Copy link
Contributor

sosoba commented Jan 5, 2024

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.

@ljharb
Copy link
Member

ljharb commented Jan 6, 2024

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.

@sosoba
Copy link
Contributor

sosoba commented Jan 12, 2024

Since Node 20.11.0:

if ( import.meta.filename === process?.argv[1] ) {
  // Node main script
}

@tschaub
Copy link

tschaub commented Jan 13, 2024

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.

@guybedford
Copy link
Contributor

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.

@egasimus
Copy link

egasimus commented Jan 13, 2024

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.1 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.)

Footnotes

  1. 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?)

@nex3
Copy link

nex3 commented Feb 20, 2024

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?

@aduh95
Copy link
Contributor

aduh95 commented Feb 21, 2024

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 or process.cliMain so that the same API can be used everywhere?

What would be the value of process.cliEntryPoint? How do you envision using it?

@egasimus
Copy link

🦗 🦗 🦗

So what's wrong with using the default export of the file passed to node as the entrypoint?

@aduh95
Copy link
Contributor

aduh95 commented Feb 21, 2024

@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. #32223 (comment) 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.

@nex3
Copy link

nex3 commented Feb 21, 2024

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.

@aduh95
Copy link
Contributor

aduh95 commented Feb 22, 2024

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.

@nex3
Copy link

nex3 commented Feb 22, 2024

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 (#51840).

@sosoba
Copy link
Contributor

sosoba commented Jul 5, 2024

Welcome in 2024

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
esm Issues and PRs related to the ECMAScript Modules implementation. feature request Issues that request new features to be added to Node.js.
Projects
Development

Successfully merging a pull request may close this issue.