[RFC]: Bringing `minimist` Dependency In-House
- Dominant language
- JavaScript
- Stars
- 6k
- Forks
- 1.3k
- Avg merge
- 1d 3h
- Merged PRs (30d)
- 611
Description
# Replace `minimist` Dependency with In-House `@stdlib/cli/parse-args`
Replace the third-party `minimist` dependency with a stdlib-native equivalent to reduce supply-chain security risk and bring all production code into conformance with stdlib conventions (docs, tests, examples, benchmarks, backward-compatibility).
## Background
- **[`minimist`](https://github.com/minimistjs/minimist)** is used exclusively as a command-line argument parser
- **Primary consumer**: [`@stdlib/cli/ctor/lib/main.js`]— the central CLI class used by every stdlib package with a CLI
- **Secondary consumers**: ~12 internal tool scripts under `@stdlib/_tools/package-json/scripts/` and `@stdlib/_tools/modules/pkg-deps/bin/cli`
- The existing code has a [TODO comment]: `// TODO: replace with stdlib equivalent`
- The minimist source is 264 lines — straightforward to reimplement
### API Surface Analysis
The full minimist API supports these options: `boolean`, `string`, `alias`, `default`, `stopEarly`, `--` separator, `unknown` callback, and dotted key expansion.
**What stdlib actually uses** (confirmed by auditing every `cli_opts.json` and direct call site):
| Option | Used? | Example |
|---|---|---|
| `boolean` | ✅ Yes | `["help", "version"]` |
| `string` | ✅ Yes | `["split", "aliases"]` |
| `alias` | ✅ Yes | `{ "help": ["h"], "version": ["V"] }` |
| `default` | ❌ No | — |
| `stopEarly` | ❌ No | — |
| `unknown` | ❌ No | — |
| `--` separator | ❌ No | — |
> [!IMPORTANT]
> **Decision**: Implement the full minimist API (all 7 options) for backward-compatibility and completeness, but prioritize testing the 3 options actually used. This ensures the replacement is a true drop-in and supports any future stdlib packages that might need additional options.
---
## Proposed Changes
### 1. New Package: `@stdlib/cli/parse-args`
Create a brand-new stdlib package that reimplements minimist's `parseArgs(args, opts)` function.
#### Folder Structure
```
lib/node_modules/@stdlib/cli/parse-args/
├── package.json
├── README.md
├── lib/
│ ├── index.js # Standard stdlib entry point
│ └── main.js # Core argument parser implementation
├── test/
│ ├── test.js # Main export, basic parsing, type checks
│ ├── test.bool.js # boolean option, --no- prefix, allBools
│ ├── test.string.js # string option, number coercion suppression
│ ├── test.alias.js # alias option, bidirectional resolution
│ ├── test.default.js # default option
│ ├── test.long.js # Long flags (--key, --key=val, --key val)
│ ├── test.short.js # Short flags (-k, -abc, -k=val, -k val)
│ ├── test.dash.js # -- separator and opts['--']
│ ├── test.stop_early.js # stopEarly option
│ ├── test.unknown.js # unknown callback
│ ├── test.num.js # Number coercion
│ └── test.proto.js # Prototype pollution prevention
├── benchmark/
│ └── benchmark.js # Performance benchmarks
├── examples/
│ └── index.js # Runnable usage examples
└── docs/
└── types/
├── index.d.ts # TypeScript declarations
└── test.ts # TypeScript compile-time tests
```
---
#### main.js
Core implementation. The function signature:
```js
function parseArgs( args, opts ) → { _: Array, ...flags }
```
Behavior to reimplement (in order of parsing priority):
1. **`--key=value`** — long flag with inline value
2. **`--no-key`** — long flag negation → sets `key: false`
3. **`--key value`** — long flag with next-arg value
4. **`--key`** (boolean) — long boolean flag → `true`
5. **`-abc`** — short flag grouping (each letter is a boolean flag)
6. **`-k value`** — short flag with next-arg value
7. **`-k=value`** — short flag with inline value
8. **`--`** — stop parsing, rest goes to `_` (or `argv['--']`)
9. **Positional args** — everything else goes to `_`
10. **Number coercion** — numeric strings → Number (unless `string` option set)
11. **Alias expansion** — set value on all aliases simultaneously
12. **Default values** — apply defaults for keys not explicitly set
13. **`stopEarly`** — after first positional, everything goes to `_`
14. **`unknown` callback** — called for undefined flags; if returns `false`, skip
15. **Prototype pollution guard** — reject `__proto__` and `constructor`
### 2. Update Consumer: `@stdlib/cli/ctor`
#### [MODIFY] @stdlib/cli/ctor/lib/main.js
Single-line change on line 25:
```diff
-var parseArgs = require( 'minimist' ); // TODO: replace with stdlib equivalent
+var parseArgs = require( '@stdlib/cli/parse-args' );
```
---
### 3. Update Consumers: Internal Tool Scripts
#### [MODIFY] 12 files under `@stdlib/_tools/`
Same single-line change in each file:
```diff
-var parseArgs = require( 'minimist' );
+var parseArgs = require( '@stdlib/cli/parse-args' );
```
Files to update:
- `_tools/modules/pkg-deps/bin/cli`
- `_tools/package-json/scripts/update_directories`
- `_tools/package-json/scripts/update_license`
- `_tools/package-json/scripts/update_engines_node_version`
- `_tools/package-json/scripts/update_homepage`
- `_tools/package-json/scripts/update_author`
- `_tools/package-json/scripts/update_os`
- `_tools/package-json/scripts/update_bugs`
- `_tools/package-json/scripts/update_contributors`
- `_tools/package-json/scripts/update_engines_npm_version`
- `_tools/package-json/scripts/update_gypfile`
- `_tools/package-json/scripts/update_repository`
---
### 4. Remove `minimist` from Dependencies
```
npm uninstall minimist
```
---
## Verification Plan
### Automated Tests
1. **New package unit tests** (primary verification):
```bash
make TESTS_FILTER=".*/cli/parse-args/.*"
```
2. **Existing `@stdlib/cli/ctor` tests** (regression — must continue passing after swap):
```bash
make TESTS_FILTER=".*/cli/ctor/.*"
```
3. **Equivalence test** — Run the existing minimist test suite against our implementation to verify behavioral parity:
```bash
# We can create a shim that points minimist's tests at our implementation
NODE_PATH=lib/node_modules node -e "var parse = require('@stdlib/cli/parse-args'); console.log(JSON.stringify(parse(['--foo', 'bar', '-abc', '--no-debug', '--count=5'])))"
```
### Manual Verification
4. **End-to-end CLI test** — Run a real stdlib CLI command and verify it still works:
```bash
NODE_PATH=lib/node_modules node bin/cli is-leap-year 2024
# Expected: true
NODE_PATH=lib/node_modules node bin/cli is-leap-year 2023
# Expected: false
NODE_PATH=lib/node_modules node bin/cli --help
# Expected: prints usage text
NODE_PATH=lib/node_modules node bin/cli --version
# Expected: prints version
```
Contributor guide
Assessment
This issue has not been assessed yet.