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.command #51538

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

guybedford
Copy link
Contributor

This implements a new import.meta.command as an alternative naming for import.meta.main. The justification for renaming is clarity in the definition of when this property applies for a given module, after the extensive discussion in #49440.

The specification for this field is provided at the same time to the WinterCG repo for import.meta in wintercg/import-meta-registry#4.

@nodejs-github-bot
Copy link
Collaborator

Review requested:

  • @nodejs/loaders

@nodejs-github-bot nodejs-github-bot added lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run. labels Jan 22, 2024
@GeoffreyBooth
Copy link
Member

#49440 spans four years and has 91 comments. Do you mind summarizing the outcome or lack thereof?

In particular, it feels like it would be unfortunate to ship something that is just like import.meta.main but isn’t import.meta.main. Especially since import.meta.main is already shipped (and long established) in Deno, my preference would be to find a consensus within Node to ship the same, and failing that to vote on whatever seems like the most reasonable proposals for how import.meta.main should be defined here. If there’s no way to define it in a way that provides equivalence with other runtimes, then I guess import.meta.command makes sense, but I’d prefer to take another shot at trying to uphold standards if we can. We’ve gotten more flexible about import.meta in recent months, especially now that WinterCG exists and we have processes in place; I wouldn’t assume that whatever inconclusiveness that played out since 2019 on the other thread is necessarily going to happen again now.

@GeoffreyBooth GeoffreyBooth added the esm Issues and PRs related to the ECMAScript Modules implementation. label Jan 22, 2024
@guybedford
Copy link
Contributor Author

@GeoffreyBooth the first few comments before the issue was closed pretty much summarize it I think, it's not a long read, it's the comments that follow that are.

Do you mind summarizing the outcome or lack thereof?

The justification for renaming is clarity in the definition of when this property applies for a given module

Deno just defines it as "the entry point" which IMO is not clearly defined. By clearly stating this is equating with the POSIX command model we rule out many different cases where there might previously be entry point ambiguity.

@devsnek would be great to hear your opinion on this one again if you have any thoughts to share.

Copy link
Member

@GeoffreyBooth GeoffreyBooth left a comment

Choose a reason for hiding this comment

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

This needs docs. In particular, I’m curious how you will describe which “entry point” gets import.meta.command when there are multiple possibilities, for example what we normally think of as the entry point but also --require or --import modules that load earlier. Is it always process.argv1? Always the very first loaded module? Each of them?

Also what about workers?

lib/internal/modules/run_main.js Outdated Show resolved Hide resolved
Comment on lines 155 to 157
* This should really just be implemented as a parameter, but executeUserEntryPoint is
* exposed publicly as `runMain` which has backwards-compatibility requirements, hence
* this approach.
Copy link
Member

Choose a reason for hiding this comment

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

Would it not be backward-compatible to just add an extra parameter at the end?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've updated the comment to note this is not actually a backwards compat concern so much as a concern that we mustn't expose setting the command main to userland as it is an entirely internally-defined concept.

Copy link
Member

Choose a reason for hiding this comment

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

Surely there’s a less awkward workaround than calling a function that sets a module-scope property ahead of time? Since runMain gets called multiple times, like for --import and so on, this approach feels error-prone. Perhaps the additional parameter could be an undocumented options object, and Node core checks that this object contains a particular symbol on it? And this symbol would be something that’s defined internally and therefore unusable by user code.

test/es-module/test-esm-command.mjs Outdated Show resolved Hide resolved
test/es-module/test-esm-command.mjs Show resolved Hide resolved
@GeoffreyBooth
Copy link
Member

the first few comments before the issue was closed pretty much summarize it

What, this? Essentially that it’s a bad idea to use this pattern? In my mind that’s a very weak argument; if other runtimes offer this, we should do so as well for compatibility, and that’s far more important. Perhaps there are other patterns that have advantages over this one, but there’s nothing inherently wrong with doing everything in one file, and plenty of scripting use cases where that’s desirable.

That thread was also just an issue, not a PR, so it doesn’t have the specifics that this PR has of exactly how the new property gets implemented and when it gets applied. Perhaps this specificity will help cut through some of the debate.

In particular, could we simply rename command to main in this PR and be in compliance with the WinterCG definition of import.meta.main? If not, why not?

@guybedford
Copy link
Contributor Author

guybedford commented Jan 22, 2024

In particular, could we simply rename command to main in this PR and be in compliance with the WinterCG definition of import.meta.main? If not, why not?

Unfortunately Deno sets it to true within a Worker in this scenario:

main.mjs

import { Worker } from 'node:worker_threads';
new Worker('./worker.mjs');
console.log('main', import.meta);

worker.mjs

console.log('worker', import.meta);

deno run -A main.mjs:

main [Object: null prototype] {
  url: "file:///C:/Users/Guy/Projects/node/test.mjs",  
  main: true,
  resolve: [Function (anonymous)]
}
worker [Object: null prototype] {
  url: "file:///C:/Users/Guy/Projects/node/worker.mjs",
  main: true,
  resolve: [Function (anonymous)]
}

Furthermore we are also defining it not to apply for any module registry other than the top-level loader. So all other realms, workers etc would not get this property.

@GeoffreyBooth
Copy link
Member

Unfortunately Deno sets it to true within a Worker

I guess what would be the harm if we did the same? Or more broadly, are there any cases where we wouldn’t want to just copy Deno’s behaviors?

@guybedford
Copy link
Contributor Author

I guess what would be the harm if we did the same? Or more broadly, are there any cases where we wouldn’t want to just copy Deno’s behaviors?

I think Deno and Bun have more loosely defined the concept of entry point here as any top-level execution context, whereas this definition is confined to the concept of the Node.js JS module file representing the running process as implemented in JavaScript.

This is all edge case territory - counter examples would be pretty obscure.

I feel like I've said enough though, I think it would be good to get a wider perspective before we drown out the discussion here as well. I'm open to alternatives.

@devsnek
Copy link
Member

devsnek commented Jan 22, 2024

would be great to hear your opinion on this one again if you have any thoughts to share.

at this point i think my (NON BLOCKING!!!) opinion would be that coming to agreement in wintercg (or wherever else) about what this api should be is probably the most healthy thing that could happen for the ecosystem. if import.meta.command is the thing then that's cool, but if its import.meta.main and nodejs is the odd one out, that seems unfortunate.

@GeoffreyBooth
Copy link
Member

I agree with @devsnek that it would be unfortunate if this were to diverge from import.meta.main on other runtimes; however in discussing with @guybedford it seems that the definition of import.meta.main used elsewhere (where the root of a worker gets main too, etc) doesn’t satisfy the use cases for our users.

I think rather than creating something a new import.meta property that’s very similar to import.meta.main but slightly different in name and definition, we could instead create a new property on process. Per this PR it could be process.command, or building off of the suggestions in #49918 it could be process.entryPoint; whatever we call it, it could have the same rules as import.meta.command does in this PR in that it applies only for the main process entry point and not other entry point candidates such as the entry of a worker or of a VM. Another advantage of putting it on process is that it would apply equally in CommonJS. It would be similar to process.argv[1] but set via slightly different rules (that I won’t attempt to define, but @guybedford should 😄 ). And users could check if the current module is the process main entry point via if (process.command === import.meta.filename) in ESM or via if (process.command === __filename) in CommonJS.

@JakobJingleheimer
Copy link
Contributor

Preface: I've only had time to read a couple dozen messages, not all of them.

process.command seems preferable to me.

When I first read import.meta.command I expected it to run a command.

I don't know why, but I intuitively understood process.command to be the command that started the process.

@GeoffreyBooth
Copy link
Member

process.command seems preferable to me.

My only concern with this name is that it sounds like it should be the entire command, like all the arguments and flags etc. Hence maybe process.entryPoint or process.argEntry.

@aduh95
Copy link
Contributor

aduh95 commented Jan 23, 2024

I don't know why, but I intuitively understood process.command to be the command that started the process.

Isn't that process.argv[1]? I'm not sure I understand what value we'd expect to find in here and how to use to determine if we're inside the CLI entry point or not.

@aduh95
Copy link
Contributor

aduh95 commented Jan 23, 2024

Worth noting that process.mainModule is already a thing (although it's doc-deprecated), and it sounds like process.command would be very close to that. What would be the difference? process.command is a URL string and process.mainModule is a path string? (Maybe process.mainModuleURL would be a better name?)

@guybedford
Copy link
Contributor Author

The edge cases are basically workers, module registries in other realms but on the same thread (shadowrealm) and even other module registries in the same realm (vm.Module, eventually to be TC39 loaders).

The argument is that in all of these other module registry contexts, that do not correspond to that one specifier in that one top-level module registry, this property should not be set.

If supporting process.command as a value, it would have to have that same property. The possible risk there then being that when we get to the situation of multiple loaders on the same thread and realm, you'd need to "reinstance" the process builtin to ensure this isn't set incorrectly.

I think that concern combined with the superior ergonomics justifies keeping this feature on import.meta. Also note that import.meta.command would only be set for that module, and the key doesn't even need to exist on other import.meta (since they are different objects anyway, functional polymorphism performance degradation via shape divergence is not a concern for import.meta in permitting optional keys).

@GeoffreyBooth
Copy link
Member

you'd need to "reinstance" the process builtin to ensure this isn't set incorrectly.

Two other options:

  1. process.isMainEntry similar to worker_threads.isMainThread.

  2. process.isMainEntry(path | URL) => boolean. Would need to return different values when called from main thread versus worker/other.

I'm hesitant to put something on import.meta if it's at all avoidable, because of the potential confusion with import.meta.main.

@aduh95
Copy link
Contributor

aduh95 commented Jan 24, 2024

  1. process.isMainEntry similar to worker_threads.isMainThread.

The obvious difference between those two values is that one is stable on a given thread, the other one is not (there's simply no way to have two modules read two different values for the same property, try to think how we could implement that).

2. process.isMainEntry(path | URL) => boolean. Would need to return different values when called from main thread versus worker/other.

This is assuming there will never be a world where the same URL could load two different modules. The ES spec already allows that using import attributes, so that seems like a rather fragile design.

because of the potential confusion with import.meta.main.

Since Node.js does not implement import.meta.main, that doesn't seem to be much of a problem, no?

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. lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants