nodejs / node

Node.js JavaScript runtime ✨🐢🚀✨
https://nodejs.org
Other
106.58k stars 29.06k forks source link

import.meta.main #49440

Open MylesBorins opened 5 years ago

MylesBorins commented 5 years ago

Deno just added this.

Should we?

https://github.com/denoland/deno/pull/1835

bmeck commented 4 years ago

@devsnek for WASI, it is just the "main" string.

devsnek commented 4 years ago

@bmeck right, i mean the semantics of handling that main.

bmeck commented 4 years ago

We might also want to lock step with WASM interface-types so that WASM side doesn't go out of sync with ESM

ljharb commented 4 years ago

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 :-/

bmeck commented 4 years ago

@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.

tschaub commented 4 years ago

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.

devsnek commented 4 years ago

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

tschaub commented 4 years ago

@devsnek - thanks for the report. Pull requests accepted :)

aduh95 commented 4 years ago

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, 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.

FWIW Deno follows the same logic.

trusktr commented 3 years ago

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.

kaizhu256 commented 3 years ago

+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);
});
'
loynoir commented 3 years ago

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
})();
loynoir commented 3 years ago

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
    }
})()
BlueNebulaDev commented 2 years ago

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?!

millsp commented 2 years ago

Doing process.argv[1] === __filename has the advantage of being compatible with both cjs and esm. That can be handled by the bundler instead.

ljharb commented 2 years ago

@millsp ESM doesn't have __filename.

Beedeebee commented 2 years ago

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.

rmclaughlin-nelnet commented 2 years ago

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,
};
ljharb commented 2 years ago

@rmclaughlin-nelnet I believe Object.keys(require.cache).length === 1 will be true if it's the first file being evaluated.

rmclaughlin-nelnet commented 2 years ago

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.

brianjenkins94 commented 2 years ago

https://stackoverflow.com/questions/57838022/detect-whether-es-module-is-run-from-command-line-in-node

import url from "url";

if (import.meta.url === url.pathToFileURL(process.argv[1]).href) {
  // module was not imported but called directly
}
rmclaughlin-nelnet commented 2 years ago

https://stackoverflow.com/questions/57838022/detect-whether-es-module-is-run-from-command-line-in-node

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

tschaub commented 2 years ago

The es-main package accounts for this and other nuances. See https://github.com/nodejs/node/issues/49440

bricker commented 2 years ago

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:

Using 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);
});

Using a separate "cli" file

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:

  1. One fewer file to maintain.
  2. cli entrypoint.js files are repetitive and often identical.
  3. imports of dependencies are in a different file from the function that receives them. This requires a developer to update two files to add or remove a single dependency, and results in unused dependencies still being imported and passed through.
sosoba commented 1 year ago

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
Gaubee commented 1 year ago

I use (import.meta.url === ('file:///'+process.argv[1].replace(/\\/g,'/')).replace(/\/{3,}/,'///')) work in node 18+

lwr commented 1 year ago

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();
}
tschaub commented 1 year ago

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

lwr commented 1 year ago

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

tschaub commented 1 year ago

@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 commented 1 year ago

@tschaub ok, I saw it is fixed in https://github.com/tschaub/es-main/pull/28

silverwind commented 9 months ago

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 commented 8 months ago

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 commented 8 months ago

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 commented 8 months ago

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 commented 8 months ago

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 commented 8 months ago

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 commented 8 months ago

Since Node 20.11.0:

if ( import.meta.filename === process?.argv[1] ) {
  // Node main script
}
tschaub commented 8 months ago
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 commented 8 months ago

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 commented 8 months ago

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?

// 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?)

nex3 commented 7 months ago

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 commented 7 months ago

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 commented 7 months ago

:cricket: :cricket: :cricket:

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

aduh95 commented 7 months ago

@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.

nex3 commented 6 months ago

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 commented 6 months ago

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 commented 6 months ago

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

sosoba commented 2 months ago

Welcome in 2024