Skip to content

feat!: yargs is now ESM first#2451

Merged
bcoe merged 45 commits intomainfrom
yargs-to-esm
Apr 9, 2025
Merged

feat!: yargs is now ESM first#2451
bcoe merged 45 commits intomainfrom
yargs-to-esm

Conversation

@bcoe
Copy link
Member

@bcoe bcoe commented Feb 18, 2025

See: https://joyeecheung.github.io/blog/2024/03/18/require-esm-in-node-js/ for what's motivating this work.

Yargs build step to be dual-mode ESM / CJS had created a fragile build process that has been hard to upgrade over the years. I'm quite excited to remove it.

Known Breaking Changes:

  • command names are now only derived if you're using commandDir. If you are providing a module directly, a command property must be set.
  • Supported Node.js versions are now ^20.19.0 || ^22.12.0 || >=23 (there were fixes to issues with require ESM in patch releases of 20.x and 22.x which is why the complicated version range is necessary).
  • yargs no longer works as a singleton, breaking usage like yargs.parse(), yargs.argv.
  • I'm sure I've broken some other things with this major overhaul, we'll keep this in beta for a healthy length of time.

TODO:

  • Look into adding back command name detection for commandDir.
  • rewrite all docs to not use yargs as singleton (this is no longer a thing in CJS compatibility mode).
    • advanced.md
    • readme.md
    • api.md
    • examples.md

Notes

  • I left the integration tests in CJS so that the behaviour of requiring yargs in a CJS environment can be tested -- this requires Node 23.
    • Note: Node 22 still seems to be having issues on Windows.
  • cliui doesn't wrap properly because it's using an old version of wrap-ansi. TODO, unravel this problem. fixed thanks @shadowspawn.
  • removed which-module functionality (TODO: dig into what the heck I was thinking when I wrote this).
    • figured it out, see: inferring module name from path.
  • 1000 lbs 🦍 in the room, should we figure out a replacement for commandDir, perhaps using createRequire? could this be a follow up PR to reduce the scope of this work? (CC: @shadowspawn thoughts). Figured out how to make commandDir work, including inferring name of command from directory.
  • Coverage is working again and readable \o/ glad to have rollup out of the mix 🥳

BEGIN_COMMIT_OVERRIDE
feat!: yargs is now ESM first
refactor!: command names are not derived from modules passed to command.
refactor!: singleton usage of yargs yargs.foo, yargs().argv, has been removed.
build!: minimum node.js versions now ^20.19.0 || ^22.12.0 || >=23.
END_COMMIT_OVERRIDE

refactor!: commandDir removed (due to not working with ESM)
@bcoe
Copy link
Member Author

bcoe commented Feb 18, 2025

@shadowspawn this is a work in progress, but porting over tests is going pretty smoothly so far.

@shadowspawn
Copy link
Member

Oh ho, exciting!

@shadowspawn

This comment was marked as resolved.

@shadowspawn

This comment was marked as resolved.

@shadowspawn

This comment was marked as resolved.

@bcoe
Copy link
Member Author

bcoe commented Mar 16, 2025

Probably running ahead of your work in progress. I was trying the branch and found that using require from CJS and experimental-require-module did not get the function with a default import. Tested with node v22 and v23.

@shadowspawn switched to using export {foo as 'module.exports'}; for both cliui and this PR, it seems to work well.

Some cliui PRs open to add wrapping in ESM by working around dependency complications, but perhaps instead moving cliui to ESM-only and use latest dependencies would be in line with this PR?

@shadowspawn thanks for the PR on cliui, I think this is mostly addressed (although I expect we'll have some follow up PRs on that repo).

I was able to stop skipping all the tests related to string wrapping 👍

@bcoe bcoe marked this pull request as ready for review March 16, 2025 16:35
@shadowspawn
Copy link
Member

I have done a pass through whole PR and made some minor comments and questions, nothing blocking.

I plan to do a little user testing on old and new commandDir next.

@shadowspawn
Copy link
Member

shadowspawn commented Apr 5, 2025

The documentation for .commandDir describes the include and exclude options for require-directory which haven't been implemented.

yargs/docs/advanced.md

Lines 272 to 278 in ef28c98

- `include`: RegExp or function
Allow list certain modules. See [`require-directory`](https://www.npmjs.com/package/require-directory) for details.
- `exclude`: RegExp or function
Block list certain modules. See [`require-directory`](https://www.npmjs.com/package/require-directory) for details.

The previous code was passing through user supplied options object so the user could also supply other require-directory options like renamer, but that behaviour was not documented as such.

Copy link
Member

@shadowspawn shadowspawn 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 sure I've broken some other things with this major overhaul, we'll keep this in beta for a healthy length of time.

Qualified approval. I think .commandDir() still a work in progress with include/exclude to implement or deprecate. But overall and notwithstanding .commandDir(), up to at least beta quality with what I saw and tried.

Nice work!

@bcoe
Copy link
Member Author

bcoe commented Apr 8, 2025

Regarding:

The documentation for .commandDir describes the include and exclude options for require-directory which haven't been implemented.

@shadowspawn I've added back support for include / exclude, based on how I think it should work. The documentation for require-directory seemed off to me, with it documenting the same behaviour for include / exclude.

The previous code was passing through user supplied options object so the user could also supply other require-directory options like renamer, but that behaviour was not documented as such.

Given that this behaviour isn't documented in yargs, I think it's not a breaking change to remove it.

@bcoe bcoe merged commit d90af45 into main Apr 9, 2025
6 checks passed
// disable recursion to support nested directories of subcommands
if (typeof opts.recurse !== 'boolean') opts.recurse = false;
this.requireCache.add(callerFile);
const fullDirPath = join(dirname(callerFile), dir);
Copy link
Contributor

Choose a reason for hiding this comment

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

Hey, I found that commandDir has been changed to be relative from here on. I just want to know the reason. Is it related to security concerns? Because in my case, I use the commandDir feature extensively to load commands from various locations (v17.7.2), not just from a single project directory. For example, I combine project commands, home directory commands, and global directory commands. With this change, my approach has become completely unfeasible. So I'd like to know if there's an option that allows the directory to be an absolute directory.

Copy link
Contributor

Choose a reason for hiding this comment

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

Except for the above case, there is another scenario that would also fail. Put yargs in the @org/core package, and export an API to launch CLI tools. Then, add commands from another package that imports @org/core and passes the directory via the API.

Copy link
Member

@shadowspawn shadowspawn Apr 21, 2025

Choose a reason for hiding this comment

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

The intent is that the directory passed into .commandDir() can be absolute, and it is probably an oversight (bug) if an absolute path passed into .commandDir() does not work.

Have you confirmed this problem at runtime, or is this feedback from code inspection?

(I won't have time to try reproducing and look carefully until later.)

Copy link
Contributor

Choose a reason for hiding this comment

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

I prepared a demo repo for this issue. It can be reproduced. When you have time, you can give it a shot.

https://github.com/vipzhicheng/test-yargs-next

Logically, fullDirPath is callerFile dir + passed in dir, I hope it is just passed in dir, like before.

Copy link
Member

Choose a reason for hiding this comment

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

Thanks for the example repo, and I reproduced the issue as described.

High level, I suspect replacing path.join with path.resolve at some level is appropriate.

I checked original package for comparison and it does use path.resolve in implementation: https://github.com/troygoode/node-require-directory/blob/d1fd7dc2eaf02832de94dbe1af0c52271697050e/index.js#L56C1-L57C1

Copy link
Member

Choose a reason for hiding this comment

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

@vipzhicheng Would you like to make a new issue, or even a PR, or shall I open one based on this info?

Thanks for taking a look at the pre-release and reporting the issue. Much appreciated.

Copy link
Contributor

Choose a reason for hiding this comment

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

I have made a PR #2465, Please take a look.

Just like you suggested, it works in my test repo and also still passes all npm run test after the change.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants