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

esm: implement import.meta.main #32223

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open

Conversation

aduh95
Copy link
Contributor

@aduh95 aduh95 commented Mar 12, 2020

Boolean value to check if a module is run as entry point of the current
process.

ES module authors need a way to determine if their code is run as CLI or as a dependency. #49440 have some proposal, I am implementing import.meta.main because:

  • it is arguably similar to the CJS pattern:
- if (module === require.main) {
+ if (import.meta.main) {
  • Deno has already implemented it, if we don't differ that is certainly easier for users to write code that runs everywhere.
  • It was suggested originally in the import.meta proposal.

Fixes: #49440

Checklist
  • make -j4 test (UNIX), or vcbuild test (Windows) passes
  • tests and/or benchmarks are included
  • documentation is changed or added
  • commit message follows commit guidelines

@nodejs-github-bot nodejs-github-bot added the esm Issues and PRs related to the ECMAScript Modules implementation. label Mar 12, 2020

* `main` {boolean} `true` when the current module is the entry point of
the current process.
* `url` {string} The absolute `file:` URL of the module.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd recommend restructuring this more like:

* {Object}
  * `main` {boolean} `true` when the current module is the entry point of
    the current process.
  * `url` {string} The absolute `file:` URL of the module.

@jasnell
Copy link
Member

jasnell commented Mar 12, 2020

What would the value be in a Worker thread?

@aduh95
Copy link
Contributor Author

aduh95 commented Mar 12, 2020

What would the value be in a Worker thread?

Good question! With the current implementation, it is true for the Worker entry point, and false for sub-modules. To check if the current script is run as CLI, that means you also have to check for isMainThread value... I'm not sure if that's the behaviour we want to implement, but it definitely needs documentation of this topic.

@aduh95 aduh95 mentioned this pull request Mar 12, 2020
Copy link
Member

@devsnek devsnek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm against this for reasons outlined in the issue

Copy link
Contributor

@jkrems jkrems left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might be convinced otherwise if there's overwhelming support for this feature but I have some concerns about this pattern (see issue comments). My concerns are broadly:

  1. Usually import.meta doesn't change based on how and when the module has been imported. It's already "magic ephemeral data", let's not make it more dynamic and magic.
  2. This pattern encourages writing code that's inherently untestable without setting up a custom module loader from scratch.
  3. (More optional preference) I would vastly prefer a solution that properly encloses optional code that is meant to run as main and doesn't also allow sprinkling that behavior into random expressions. One example I suggested was to export a main or startup function for this purpose.

@bmeck bmeck added the blocked PRs that are blocked by other issues or PRs. label Mar 12, 2020
@aduh95
Copy link
Contributor Author

aduh95 commented Mar 13, 2020

@jkrems regarding your first point, having a way to tell if current module is the entry point is a motivation clearly stated in the import.meta proposal. The way I understand it, it is supposed to be dynamic and magic.

I think your second point meets @devsnek's: using import.meta.main to run different code is not a good pattern. While I agree with this, I still firmly believe import.meta.main is a needed feature. My issue is currently when writing an ES module meant to be a CLI tool, there is no way to inform the user that they may have imported the wrong file:

import { isMainThread } from 'worker_threads';

if (!isMainThread || !import.meta.main) {
  process.emitWarning(new Error('This module should be run from CLI.'));
}

Regarding your third point, while it'd fine with me, I fail to see how exporting a main function is less magical than checking a boolean value.

@devsnek
Copy link
Member

devsnek commented Mar 13, 2020

i think package exports can disallow importing your cli entrypoint. I also believe that "you've imported the wrong file" isn't specific to cli entrypoints, there are lots of files that libraries have that shouldn't be directly imported, which is why we should seek more general solutions.

Boolean value to check if a module is run as entry point of the current
process.

Fixes: https://gtihub.com/nodejs/modules/issues/274
Copy link
Member

@addaleax addaleax left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m good with this, personally.

@aduh95
Copy link
Contributor Author

aduh95 commented Aug 27, 2020

Do we want to reconsider this? Asking because it's been recently brought up in #34882, it seems there are users looking for it.

@guybedford
Copy link
Contributor

The use of import.meta.main is now highly established in the Deno ecosystem so I think it would be very benefical to move forward with this.

The other benefit of import.meta.main is it has a graceful failure when not supported, unlike most other import.meta features.

I think treating any new context as supporting import.meta.main makes sense as well so am happy with the worker support in this PR.

One pattern I also really like with import.meta.main is using it for creating in-module unit tests:

export function lib () {
  // ...implementation..
}

if (import.meta.main) {
  assert.strictEqual(lib('unit'), 'test');
}

where the tests are run by just running node src/lib.js for each internal module individually.

During a build, import.meta.main can then be optimized out as it can be completely statically inferred for the build without any extra metadata.

@jkrems perhaps that counters your untestable argument in (2) :) I do like the idea of having an exports condition, but that seems complementary to this proposal. Would seeing progress on that help overcome your and @devsnek's concerns here?

@jkrems
Copy link
Contributor

jkrems commented Oct 5, 2020

During a build, import.meta.main can then be optimized out as it can be completely statically inferred for the build without any extra metadata.

What I think is very unfortunate about those kinds of tests, is that they'll never run in a browser without building. So it's effectively creating a test pattern that is browser-incompatible. Not something that node has to care about but I would prefer if we wouldn't encourage the creation of node-only (or bundler-only) code.

perhaps that counters your untestable argument in (2)

It's still "untestable" with the exception of exactly this case: The code inside of the block is a test itself, will only ever run as a subprocess, and doesn't require any testing/assertion framework (block-scoped import is way less convenient than block-scoped require). It doesn't allow running multiple tests inside of the same process (or loading them through a test runner bootstrap).

The use of import.meta.main is now highly established in the Deno ecosystem so I think it would be very benefical to move forward with this.

Afaik deno is still much smaller as an ecosystem than nodejs (let alone the browser). So adding a module system feature to nodejs because deno is doing it when the feature cannot be used in browsers (likely in perpetuity), isn't really convincing to me.

All that being said: These are all ecosystem concerns, not nodejs concerns. I'd be willing to dismiss my block to this change if there's a way to import a module as main without spawning a subprocess. That would make it somewhat testable at least and enable simple wrapper binaries.

@aduh95
Copy link
Contributor Author

aduh95 commented Oct 5, 2020

The code inside of the block is a test itself, will only ever run as a subprocess[…]. It doesn't allow running multiple tests inside of the same process (or loading them through a test runner bootstrap).

Technically, it could be run on the same process using Workers.

adding a module system feature to nodejs because deno is doing it when the feature cannot be used in browsers (likely in perpetuity), isn't really convincing to me.

I suppose the web could also implement import.meta.main which would be true iif the module is run as web worker entry point. I reckon that's very speculative though, but if the feature gets traction from the community, I don't see why it couldn't happen.

If I'm being honest, the same argument applies to export function main pattern. Since there is no consensus amoung us, I was thinking maybe we could implement both behind a flag, then wait and see which gets the more popular. What do y'all think?

@jkrems
Copy link
Contributor

jkrems commented Oct 5, 2020

If I'm being honest, the same argument applies to export function main pattern.

I don't think that's true. It's fairly simple to run a file using export main in the browser:

import {main} from './entrypoint.js';
main();

There's no need to parse & transform entrypoint.js, it works exactly as-is. With import.meta.main, entrypoint.js has to be parsed and transformed. It cannot be served directly with the contents the user wrote.

@aduh95
Copy link
Contributor Author

aduh95 commented Oct 5, 2020

I don't think that's true. It's fairly simple to run a file using export main in the browser:

Yes sorry, I meant the fact that the HTML standard might implement import.meta.main is also true for export function main and is not a good argument for either at this point.

I think you're making a valid point with the browser compatibility thing, and I don't have any argument to counter that 😅

@jkrems
Copy link
Contributor

jkrems commented Oct 5, 2020

Ah yes, sorry. Misread your comment about native browser support. :)

@ExE-Boss
Copy link
Contributor

ExE-Boss commented Oct 5, 2020

@jkrems
import.meta.main will simply be undefined in the browser.

The build step is about removing if (import.meta.main) { … } if statements in bundled modules that are not the entry module, because import.meta is shared in the context of the bundle:

Example:

Input:

// src/foo.js
if (import.meta.main) {
	throw new Error(`${import.meta.url} is not an entrypoint module`);
}

export function foo() { /* ... */ }
// src/main.js
import { foo } from "./foo.js";
export * from "./foo.js";

if (import.meta.main) {
	console.log(foo());
}

Output:

// dist/bundle.js
// src/foo.js
function foo() { /* ... */ }

// src/main.js
if (import.meta.main) {
	console.log(foo());
}

export { foo };

@jkrems
Copy link
Contributor

jkrems commented Oct 5, 2020

import.meta.main will simply be undefined in the browser.

The build step is about removing if (import.meta.main) { … } if statements in bundled modules that are not the entry module, because import.meta is shared in the context of the bundle:

Yes, that's the problem. In the example above (where the thing inside of import.meta.main is a test), it's easy not to run the test but if I do want to run the test in a browser, only bundling could make the test actually run. In other words: The code in that block is 100% browser in-compatible. There's no API in the browser that could make the test usable if it's wrapped in import.meta.main.

@guybedford
Copy link
Contributor

@jkrems it will likely be down to something like VM / compartments / realms to allow defining import.meta property hooks. Alternatively we could have a loader hook for this. The problem is on the one hand we are saying we can't add import.meta properties unless they can be customized, yet with the other we are stopping customization because we are saying they should be specified, but there is no way to specify anything for import.meta as the standard falls in the gap between standards bodies! So there we go.

@jkrems
Copy link
Contributor

jkrems commented Oct 6, 2020

it will likely be down to something like VM / compartments / realms to allow defining import.meta property hooks.

I think that really doesn't address the usability concerns, just like loader hooks in node wouldn't. E.g. let's say somebody wrote two worker entrypoints, both using import.meta.main to guard the main logic. Now I want to run both in the same worker. Afaict with import.meta.main I just cannot. Even if there would be a loader hook, I would now have to write a custom loader hook just to compose two functions (effectively).

I get that the current UX (require.main) isn't great either - but at least it's relatively simple to load a CJS module as main (not that it's a great idea to do it repeatedly), the API is available on Module. But import.meta.main doesn't have any API to do it and the only API suggested so far (loader hook) would require starting the entire process with different flags and inspecting every single module load to match the one or two targeted modules.

Maybe there could be an API like Module.importAsMain(url)? Still doesn't work in browsers but at least allows composition in node without jumping through too many hoops.

@rektide
Copy link

rektide commented Feb 25, 2021

please ship this. it's a huge help for a problem countless people run into regularly. allegedly it has risks and/or may not be 100% perfect for every use case (frankly i don't understand most of the concerns) but this seems straightforward & super helpful in 99.99% cases. not shipping it is really hurting us all very much. if we need a more foibled fancy alternative, let it emerge over time on some other import.meta. give us this straightforward common sense help which we very much need now.

@guybedford
Copy link
Contributor

This PR is currently blocked by two members. @devsnek and @jkrems, would you be able to reconsider or restate your positions on this feature? I'd be glad to rebase and get this going again at any point.

@jkrems
Copy link
Contributor

jkrems commented Mar 6, 2021

would you be able to reconsider or restate your positions on this feature? I'd be glad to rebase and get this going again at any point.

I'd like to make sure that there's a way to wrap a module that uses import.meta.main. E.g. I wouldn't want to land this if there's not at least a very strong possibility of a follow-up to allow that. I mentioned as an example something like Module.importAsMain. It still doesn't allow for many things that something like export function main would, but at least it doesn't require booting up a completely new process just to run code guarded by import.meta.main wrapped in some setup/bootstrapping logic.

What I mean here is the equivalent of CJS:

const originalBinary = require.resolve('.bin/some-app');
doSomeSetup();
require('module')._load(originalBinary, null, true);

I don't think that should require a loader hook and forking a new process with ESM.

@aduh95
Copy link
Contributor Author

aduh95 commented Mar 6, 2021

@jkrems With this PR implementation, you can do: new Worker('./moduleAsMain.mjs'). Does this address your concern?

@jkrems
Copy link
Contributor

jkrems commented Mar 6, 2021

Not really since the code in question may expect to be the main process and require control of things like process.exit (or worker thread APIs) in a way incompatible with running as a worker. That should be very likely for code that uses import.meta.main in the first place.

@guybedford
Copy link
Contributor

I'd like to make sure that there's a way to wrap a module that uses import.meta.main

We have the same problem here for any custom behaviour with loader hooks, and being able to dynamically construct a loader seems like it would be the ideal general solution at this level. For now we at least have VM that can do this. If we had some great createRealm({ loader: 'path/to/loader.mjs' }).import() API would you still have this concern?

@jkrems
Copy link
Contributor

jkrems commented Mar 8, 2021

If we had some great createRealm({ loader: 'path/to/loader.mjs' }).import() API would you still have this concern?

Likely less. Things that act as entrypoints likely want to run as the entrypoint, in the exact environment that an entrypoint would see. Running in a separate realm with different globals from the ones used by node core (at least for builtins) may break the code. It may work but it's both more complicated (write a new loader, worry about collisions with other loaders already active on the initial realm) and more fragile (because it changes the JS environment of the original entrypoint in subtle ways), compared with the CJS equivalent (see above) or the nonimport.meta.main ESM equivalent ("just import the entrypoint that has top-level code").

@guybedford
Copy link
Contributor

Perhaps we aren't clear on the meaning of import.meta.main? My assumption personally is it's more of a import.meta.cli_invocation_this_is_the__main_process_module flag, where setting it virtually would be customizable through generic hooks. I wouldn't expect it to apply to workers or any other VM entry points without such configurations provided explicitly. I haven't tested this cases in Deno though so can't say.

@devsnek
Copy link
Member

devsnek commented Mar 8, 2021

@guybedford i'm still fundamentally opposed to this feature. even among people who want it, there is disagreement about what qualifies as "main".

With TLA you could do if (await import.meta.resolve(process.argv[1]) === import.meta.url) which is quite unambiguous.

@atg
Copy link

atg commented Sep 8, 2021

i'm still fundamentally opposed to this feature. even among people who want it, there is disagreement about what qualifies as "main".

If act of precisely specifying this feature has identified situations where the value of require.main is wrong, then sure go ahead and fix it in import.meta.main, but it seems that the exact semantics of require.main have not been a major pain point in the last 10 years, for most people require.main works just fine and they aren't running into these corner cases. So just decide on some semantics that are acceptable and ship it (please).

@jkrems
Copy link
Contributor

jkrems commented Sep 8, 2021

it seems that the exact semantics of require.main have not been a major pain point in the last 10 years, for most people require.main works just fine and they aren't running into these corner cases.

One of the semantics of require.main is that it can be true for files imported in userland by using APIs exposed by node. If that wouldn't be true, people absolutely would run into corner cases, e.g. babel-node file.js would be hard or even impossible to implement somewhat efficiently (similar for other tools that want to be entry point wrappers). So it's true but the exact semantics of require.main have worked out okay. This implementation of import.meta.main doesn't replicate them though.

@aduh95
Copy link
Contributor Author

aduh95 commented Sep 8, 2021

This implementation of import.meta.main doesn't replicate them though.

Why though? If someone is writing a custom loader, it doesn't seem very complicated to replace import.meta.main with true or false in the source code.

@jkrems
Copy link
Contributor

jkrems commented Sep 8, 2021

Why though? If someone is writing a custom loader, it doesn't seem very complicated to replace import.meta.main with true or false in the source code.

I've written custom entry point wrappers in the past that didn't involve any source transformation. Setting the bar at "needs to do contextual compilation of all source code" seems excessive, compared to the status quo. It also makes it impossible to use node-profiling-wrapper main.js without worrying about interactions with "real" loaders that may be present.

It's fine if everybody agrees that this is an acceptable regression in the name of replicating the same API "look". But I think it's a meaningful difference that makes this not "just like" require.main. Again, fine to accept this difference. But I don't think it's valuable to just pretend like it doesn't exist.

@RedYetiDev
Copy link
Member

Hey, this issue has been inactive for a bit, and recent issues have surfaced that reference this feature, is this still planned on being added / blocked?

CC @timfish #53882

@guybedford
Copy link
Contributor

This sounds distinct from what is being discussed in #53882. It might even be a separate property like import.meta.preloading?

@RedYetiDev
Copy link
Member

This sounds distinct from what is being discussed in #53882. It might even be a separate property like import.meta.preloading?

I'm not saying it's the solution do that problem, I'm just saying it was referenced, and the sources for the feature (this) are seemingly stale, so I wanted to verify they were still being worked on. (Sorry if I worded my last comment poorly)

@GeoffreyBooth
Copy link
Member

I think since Deno supports this property we should add it if we can. I think we should go through the same process with the WinterCG registry that we went through for import.meta.filename, to ensure that our implementation is compatible with Deno's.

@RedYetiDev RedYetiDev removed the blocked PRs that are blocked by other issues or PRs. label Sep 13, 2024
@RedYetiDev
Copy link
Member

(AFAICT there are no specific issues/PRs blocking this issue)

@ljharb
Copy link
Member

ljharb commented Sep 14, 2024

The block is because of the still unresolved request for changes on the PR; it should remain.

@RedYetiDev
Copy link
Member

The blocked label is specific for issues/PRs blocking something, so I was trying to declutter. Feel free to re-add if needed.

@ljharb
Copy link
Member

ljharb commented Sep 14, 2024

I dont have the ability to re-add it; it’s also blocked on WinterCG though.

@aduh95
Copy link
Contributor Author

aduh95 commented Sep 14, 2024

it’s also blocked on WinterCG though.

Is it? AFAICT import.meta.main is already defined in https://github.com/wintercg/import-meta-registry, not sure what other kind of input we should expect from WinterCG.

@ljharb
Copy link
Member

ljharb commented Sep 14, 2024

That’s just documentation; it’s not the same as WinterCG having agreed on an interoperable implementation.

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.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

import.meta.main