Skip to content

fix: stop manifest search on devEngines package managers#811

Open
michkot wants to merge 12 commits intonodejs:mainfrom
michkot:fix/issue-779-devengines-lookup
Open

fix: stop manifest search on devEngines package managers#811
michkot wants to merge 12 commits intonodejs:mainfrom
michkot:fix/issue-779-devengines-lookup

Conversation

@michkot
Copy link
Copy Markdown

@michkot michkot commented Apr 18, 2026

fixes #779

Summary

This branch is intentionally a small stack, not a single behavioral change.

The main fix is the manifest-selection bug from #779: when scanning parent directories, loadSpec now stops at the nearest package.json that defines either packageManager or devEngines.packageManager. Before that, the scan only stopped for packageManager, so a closer manifest using only devEngines.packageManager could be skipped and a parent manifest could win instead.

The branch then adds two explicit follow-up behavior changes that became clearer while fixing the lookup:

  • top-level packageManager: "" and packageManager: null are now treated as defined but invalid values of the nearest manifest, so lookup stops there and reports an invalid "packageManager" instead of treating those values as absent and falling through to a parent package.json
  • Engine.findProjectSpec now honors non-error onFail for package-manager name mismatches when the selected project package manager comes only from devEngines.packageManager; in those cases, onFail: warn / ignore can fall back to the requested package manager instead of throwing

The branch also improves invalid "packageManager" errors so they mention which field in the selected package.json was used.

Decision table

The table below describes the outcome for the nearest selected manifest. It intentionally ignores transparent-command behavior, which can still choose fallback for separate reasons.

packageManager devEngines.packageManager Effective onFail Selected-manifest result Engine.findProjectSpec result
absent absent n/a continue scanning upward if nothing else is found: NoSpec -> use fallback
valid absent n/a stop and use packageManager enforce that package manager
invalid absent n/a stop and report invalid packageManager throw invalid packageManager error
absent valid error stop and use devEngines.packageManager enforce that package manager; mismatch throws
absent valid warn / ignore stop and use devEngines.packageManager mismatch falls back to requested package manager
absent invalid error stop and report invalid devEngines throw invalid devEngines.packageManager error
absent invalid warn / ignore stop at nearest manifest and return NoSpec use fallback
valid valid and matching any stop and use packageManager enforce packageManager
valid valid but mismatching error stop and report field mismatch throw mismatch error
valid valid but mismatching warn / ignore stop and use packageManager enforce packageManager
valid invalid error stop and report invalid devEngines throw invalid devEngines.packageManager error
valid invalid warn / ignore stop and use packageManager enforce packageManager

Tests

  • keep one CLI regression for the mixed-field case where a child devEngines.packageManager must win over a parent packageManager
  • add Engine.findProjectSpec coverage for:
    • nearest child devEngines.packageManager when the parent has no package-manager field
    • nearest child devEngines.packageManager over a parent packageManager
    • nearest invalid child devEngines.packageManager with onFail: warn, which must stop the search and use fallback rather than a parent packageManager
    • nearest child packageManager: ""
    • nearest child packageManager: null

Notes

  • This branch changes behavior for packageManager: "" and packageManager: null: they no longer behave like an absent field during upward lookup.
  • Follow-up suggestion: onFail should probably not affect structurally invalid devEngines.packageManager definitions, for example when name is not a string or version is not a valid semver range. Those look like malformed configuration rather than mismatch-policy choices, so they likely should always throw. The mismatch cases can still remain controlled by onFail.

Commit ordering

  • the first two commits are pure repros, so reviewers can inspect the failing scenarios before reading the fix
  • the next functional commits are ordered from root cause to follow-up semantics: first nearest-manifest selection, then explicit packageManager: "" | null handling, then the onFail: warn fallback behavior, then the error-path improvement
  • the CLI coverage reduction is kept as its own test-shaping commit so the single retained end-to-end repro stays obvious
  • the final docs and refactor commits are intentionally last, so reviewers can read behavioral changes without rename / comment / control-flow cleanup noise mixed into them

@michkot michkot force-pushed the fix/issue-779-devengines-lookup branch 2 times, most recently from a148873 to 355d22d Compare April 18, 2026 04:51
@michkot michkot force-pushed the fix/issue-779-devengines-lookup branch from 355d22d to 1cc5eb0 Compare April 18, 2026 05:19
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.

Asymmetry between searching for closest package.json when packageManager is defined vs when devEngines.packageManager is defined after #643

1 participant