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

help() using yargs.default #2010

Open
SheetJSDev opened this issue Aug 17, 2021 · 13 comments
Open

help() using yargs.default #2010

SheetJSDev opened this issue Aug 17, 2021 · 13 comments

Comments

@SheetJSDev
Copy link

The original typescript code looked like:

import yargs from "yargs";
yargs(process.argv.slice(2)).help().argv;

The generated JS code looks like

var yargs_1 = require("yargs");
var args = yargs_1.default(process.argv.slice(2)).help().argv;

Save that to t.js and run node t.js --help:

$ node t.js --help
Options:
  --version  Show version number                                       [boolean]
  ----help

It adds 2 hyphens irrespective of the number of hyphens and also wraps:

$ node t.js $(printf "%420s" | sed 's/ /-/g')help
Options:
  --version                                 Show version number        [boolean]
  ----------------------------------------
  ----------------------------------------
  ----------------------------------------
  ----------------------------------------
  ----------------------------------------
  ----------------------------------------
  ----------------------------------------
  ----------------------------------------
  ----------------------------------------
  ----------------------------------------
  ----------------------help

Subsequent arguments are written with two dashes:

$ node t.js --help abc def ghi
Options:
  --version  Show version number                                       [boolean]
  ----help
  --abc
  --def
  --ghi

The same behavior occurs if you set the script to executable and prepend the shebang:

$ cat t.js 
#!/usr/bin/env node
var yargs_1 = require("yargs");
var args = yargs_1.default(process.argv.slice(2)).help().argv;
$ chmod +x t.js
$ ./t.js --help abc def ghi
Options:
  --version  Show version number                                       [boolean]
  ----help
  --abc
  --def
  --ghi
@bcoe
Copy link
Member

bcoe commented Sep 7, 2021

@SheetJSDev that's quite weird, I notice it cropping up as far back as yargs@16.2.0. Thanks for the bug report, if you have a chance to dig deeper would happily accept a patch.

@ghost

This comment has been minimized.

@shadowspawn
Copy link
Member

I tracked down some symptoms with some wild tracing, but not sure about the fix yet. When self.help() is called the options.default object contains entries for the options from the command-line (verbatim):

var yargs_1 = require("yargs");
var args = yargs_1.default(process.argv.slice(2)).help().default({ 'ddd': 'DDD' }).argv;
node index.js --foo=bar --help

Trace: options.default: { '--foo=bar': undefined, '--help': undefined, ddd: 'DDD' },

...which then appear in the help as though they were options...

Options:
  --version    Show version number                                     [boolean]
  ----foo=bar
  ----help
  --ddd                                                         [default: "DDD"]

Note the ----foo=bar along with the ----help. My theory is the --help from the command-line is masking the default help option.

@SheetJSDev
Copy link
Author

@bcoe FWIW some form of this bug actually goes all the way back to 4.0.0 (7 years ago !)

$ npm i --save yargs@4.0.0

$ cat t.js
var yargs_1 = require("yargs");
var args = yargs_1.default(process.argv.slice(2)).help().argv;
$ node t.js --help
Options:
  -0                                                         [default: "--help"]
  --help  Show help                                                    [boolean]

$ cat tt.js
var yargs_1 = require("yargs");
var args = yargs_1(process.argv.slice(2)).help().argv;
$ node tt.js --help
Options:
  --help  Show help                                                    [boolean]

@shadowspawn
Copy link
Member

shadowspawn commented Feb 22, 2023

Another datapoint is the issue occurs with the singleton, but not with a fresh instance. (Also some differences in the above comment.) Edit: and yargs.default being implicated is noted in issue title!

$ node index.js --foo=bar --help

//var yargs_1 = require("yargs");
//var args = yargs_1.default(process.argv.slice(2)).help().default({ 'ddd': 'DDD' }).argv;
Options:
  --version    Show version number                                     [boolean]
  ----foo=bar
  ----help
  --ddd                                                         [default: "DDD"]

//var yargs_1 = require("yargs");
//var args = yargs_1(process.argv.slice(2)).help().default({ 'ddd': 'DDD' }).argv;
Options:
  --version  Show version number                                       [boolean]
  --help     Show help                                                 [boolean]
  --ddd                                                         [default: "DDD"]

@shadowspawn
Copy link
Member

The help option is not special, the same issue occurs with an ordinary option too:

% node index.js --foo=bar --ddd EEE --help
Options:
  --version    Show version number                                     [boolean]
  ----foo=bar
  ----ddd
  --EEE
  ----help

@shadowspawn
Copy link
Member

shadowspawn commented Feb 22, 2023

Oh, just worked out why my reproduction is behaving oddly. When calling yargs_1.default() from JavaScript it is not the default export, is is the default() method of the exported singleton yargs parser. So adds the command-line arguments as defaults, oops.

Is that also what the generated TypeScript calls? Might just be a conflict with the default export!

Enough for tonight, to be resumed...

@shadowspawn
Copy link
Member

Ok, got to the bottom of the failure mode. I was able to reproduce the issue from a TypeScript file by commenting out the esModuleInterop line in the tsconfig.json file generated by tsc --init.

With "esModuleInterop": true the problem does not occur. The generated TypeScript adds a default property if necessary:

"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const yargs_1 = __importDefault(require("yargs"));
console.log((0, yargs_1.default)(process.argv.slice(2)).help().default('aaa', 'AAA').argv);

The example TypeScript configuration in the Yargs documentation does have "esModuleInterop": true.

I need to do some more reading to see if the Yargs implementation can play better with TypeScript here, or whether updating the Yargs documentation to be more explicit about working patterns is the best approach.

@ljharb
Copy link
Contributor

ljharb commented Feb 22, 2023

Note that TS's module system is fundamentally broken if you don't have esModuleInterop enabled, so I don't personally think it's really worth supporting another config.

@shadowspawn
Copy link
Member

shadowspawn commented Feb 23, 2023

Yargs has a complicated history of usage, and four entry points used here, and TypeScript allows various approaches, and @types/yargs is external. Doing some experimenting of permutations with types installed to check what actually happens with yargs@17.7.1.

// import * as yargs from "yargs";
// import yargs from "yargs";
// import yargs = require("yargs");
import yargs from 'yargs/yargs';
console.log(yargs(process.argv.slice(2)).help().default('aaa', 'AAA').argv);

allowSyntheticDefaultImports undefined in all cases.

esModuleInterop: false, module:CommonJS

  • import * as yargs from "yargs": good
  • import yargs from "yargs": broken, this issue
  • import yargs = require("yargs"): good
  • import yargs from "yargs/yargs": compile-time error because @types/yargs/yargs says using "export=function"

esModuleInterop: true, module:CommonJS

  • import * as yargs from "yargs": TypeError: yargs is not a function
  • import yargs from "yargs": good
  • import yargs = require("yargs"): good
  • import yargs from "yargs/yargs": good

esModuleInterop: false, module:node16

  • import * as yargs from "yargs": TypeError: yargs is not a function
  • import yargs from "yargs": good
  • import yargs = require("yargs"): good
  • import yargs from "yargs/yargs": good

esModuleInterop: true, module:node16

  • import * as yargs from "yargs": TypeError: yargs is not a function
  • import yargs from "yargs": good
  • import yargs = require("yargs"): good
  • import yargs from "yargs/yargs": good

Edit: updated July 2023 with much improved compatibility. I was not previously using module: node16 for the ESM scenarios, and that much improves compatibility across the import styles. In particular default import from yargs/yargs is working in all cases. Hurrah!

Edit: found further problems in #483 when module: Node16 and esModuleInterop: false and allowSyntheticDefaultImports: true targeting CommonJs. Which is outside the above permutations.

@shadowspawn
Copy link
Member

shadowspawn commented Feb 26, 2023

My current understanding is this is not a bug as such. Three thoughts for improvements:

  1. Could Yargs show a message if the user is likely to be in this situation? Perhaps if an array gets passed into .default(), which takes a string or an object or a function as the first argument.
  1. the documentation could be updated to emphasise esModuleInterop with the recommended patterns. There is a separate issue open about updating the getting started documentation where I have added a mention: the way we import yargs (lag of documentation) #2045
3) I think TypeScript esm `import yargs from "yargs/yargs"` might be able to play better with change to `@types/yargs`. I'll reach out on DefinitelyTyped for [discussion](https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/64487). <\strike>

@shadowspawn
Copy link
Member

shadowspawn commented Jul 15, 2023

I looked into throwing an error if .default() is passed an array. There is type checking, and it does allow an array. Hmm, does it do something?

Yup. There is an apparently undocumented behaviour that can pass an array of keys and have them set to same value. Can't detect the broken TypeScript import without breaking something else. So current plan is update the documentation and hope people trip over the import problem less!

Example of using array:

var yargs = require("yargs");
yargs(process.argv.slice(2)).default(['a', 'b'], 3).argv;
% node array.js --help
Options:
      --help     Show help                                             [boolean]
      --version  Show version number                                   [boolean]
  -a                                                                [default: 3]
  -b                                                                [default: 3]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants