Skip to content

fix(parse): leave an omitted array option absent - #177

Merged
sxzz merged 1 commit into
cacjs:mainfrom
MFA-G:fix/array-option-undefined-when-omitted
Oct 11, 2026
Merged

sxzz merged 1 commit into
cacjs:mainfrom
MFA-G:fix/array-option-undefined-when-omitted

Conversation

@MFA-G

@MFA-G MFA-G commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Closes #175.

Problem

An option declared with type: [] but never passed on the command line ends up as [undefined] as soon as any unrelated option appears in argv:

const cli = cac()
  .option('--type <type>', 'Choose a project type', { default: 'node' })
  .option('--externals <external>', 'Add externals', { type: [] })

cli.parse(['node', 'bin'])
// { '--': [], type: 'node' }                      ✅ externals absent

cli.parse(['node', 'bin', '--type', 'node'])
// { '--': [], type: 'node', externals: [ undefined ] }   ❌

Consumers that check if (options.externals) or spread the options into a config object get a truthy one-element array of undefined instead of nothing.

Cause

In CAC#mri, setByType(options, transforms) is called once per parsed key. setByType then loops over all array-typed transforms, not just the key that was actually set:

for (const [key, transform] of Object.entries(transforms)) {
  if (transform.shouldTransform) {
    obj[key] = [obj[key]].flat()   // obj[key] may be undefined

With no parsed keys at all the loop body never runs, which is why the plain node bin case looks fine. Passing --type gives the outer loop one iteration, setByType runs, and [undefined].flat() is written to the untouched externals key.

Fix

Only transform keys that are actually present on the options object:

if (transform.shouldTransform && key in obj) {

key in obj (rather than a value check) keeps an explicitly-set falsy or undefined value transformable, and still lets a declared default be wrapped into an array as before.

Test

Added a regression test in tests/index.test.ts covering the three states: option omitted entirely, option omitted while an unrelated option is passed, and option actually passed. It fails on main ([undefined] vs absent) and passes with the fix.

Validation: pnpm vitest run → 18/18 passing, pnpm lint and pnpm typecheck clean.

setByType ran over every array-typed option on each parsed key, so an option declared with `type: []` but never passed was wrapped as `[undefined]` as soon as any unrelated option appeared in argv.

Only transform keys that are actually present on the options object.

Closes cacjs#175
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.

Omitted array option becomes [undefined] when an unrelated option is passed

2 participants