Skip to content

Fix Node dependency install quoting and failure reporting#44

Merged
UltraBob merged 1 commit into
mainfrom
fix/node-install-quoting-and-failure-reporting
Jul 24, 2026
Merged

Fix Node dependency install quoting and failure reporting#44
UltraBob merged 1 commit into
mainfrom
fix/node-install-quoting-and-failure-reporting

Conversation

@UltraBob

Copy link
Copy Markdown
Owner

Fixes #42.

The problem

The Phase 3 JS dependency install always failed, and the failure was then masked as success:

  1. Escaped semver ranges. The name@range tokens (versions resolved from Drupal core's package.json) were escaped with printf '%q' and word-split into the command array. Word splitting doesn't do quote removal, and run_command execs the array without a shell reparse, so npm received a literal cspell@\^9.8.0 and rejected it as an invalid dist-tag (EINVALIDTAGNAME). The %q predates the switch from ddev exec bash -lc "<string>" to a direct argv array, where it was still correct.
  2. Overloaded return code. maybe_install_missing_root_deps returned 1 for both "nothing missing" and "install failed", so after the failed versioned install the caller fell through to the bare ddev npm install fallback (intended for the nothing-missing case), which succeeded trivially. The installer then printed "Node toolchain installed" and listed the toolchain in the final summary.
  3. Unversioned retry guidance. Failure paths printed the package list without versions. Installed that way, npm resolves eslint-plugin-yml to 3.x (peer eslint >=9.38), which conflicts with eslint-config-airbnb-base (peer eslint 7/8) — the peer conflict reported in the issue.

Note on the issue's Bug 1: the installer's constraint set was actually consistent — eslint-plugin-yml@^1.19.1 and @^1 both resolve to 1.19.1, whose peer range (eslint >=6) accepts the pinned eslint@^8.57.1. The constraints simply never reached npm because of the escaping, and the peer conflict came from following the unversioned manual-install guidance. No version pins needed to change.

The fix

  • Pass name@range tokens to npm/yarn as raw argv elements; no escaping.
  • Give maybe_install_missing_root_deps distinct return codes (installed / failed / nothing missing / skipped) and only run the bare-install fallback when nothing was missing or the add was skipped. A failure now prints Node toolchain install FAILED, shows Node toolchain: FAILED in the summary with a numbered FIX step, and never lists the toolchain as installed — mirroring the existing PHP dev-tools failure handling (non-fatal, prominent).
  • Capture the return code at the existing-toolchain top-up call site too, so a failed top-up reports FAILED instead of reading as "already present" (found during review; same bug class, second caller).
  • Print the exact versioned retry command in failure paths and in the summary; the last-resort unversioned list now notes to take version ranges from core's package.json if conflicts appear. When the bare install of already-declared deps fails, the retry guidance is a plain ddev npm install instead of the package list.

Testing

  • New stub-based installer tests (no ddev needed): unescaped argv tokens reach npm, install failure reports FAILED end-to-end, failed top-up on an existing toolchain reports FAILED. Each verified red against the old installer, green against the fix.
  • New real-ddev tests (node tag): versioned dependency add with real ^ constraints succeeds; unresolvable constraint produces the FAILED summary while the installer still completes.
  • Full node-tagged suite and the fast installer suite pass; shellcheck removes two warnings (SC2206) and adds none; bash -n clean; no bash 4+ constructs (macOS bash 3.2 stays supported).

The versioned JS dependency install always failed because name@range
tokens were escaped with printf %q before being word-split into the
command array: npm received a literal \^ and rejected it as an invalid
dist-tag (EINVALIDTAGNAME). The failure was then masked because
maybe_install_missing_root_deps returned 1 both for "nothing missing"
and "install failed", so the caller fell through to a bare npm install
that succeeded trivially, printed "Node toolchain installed", and
listed the toolchain in the final summary.

- Pass name@range tokens to npm/yarn as raw argv elements; run_command
  execs them without a shell reparse, so no escaping is needed.
- Give maybe_install_missing_root_deps distinct return codes
  (installed / failed / nothing missing / skipped) and only fall
  through to the bare install when nothing was missing or the add was
  skipped. Failures now produce a "Node toolchain: FAILED" summary
  entry with retry instructions, matching the PHP dev-tools pattern.
- Capture the return code at the existing-toolchain top-up call site
  too, so a failed top-up is reported as FAILED instead of the
  pre-existing node_modules reading as "already present".
- Print versioned retry commands (constraints resolved from Drupal
  core) instead of the unversioned package list, which hits peer
  dependency conflicts (latest eslint-plugin-yml requires eslint >= 9
  while eslint-config-airbnb-base requires eslint 7/8), and note the
  version-alignment caveat on the static fallback list.

Add stub-based installer tests covering the unescaped argv path, the
FAILED reporting on install failure, and the existing-toolchain top-up
failure, plus real-ddev tests for the versioned dependency add and the
FAILED summary.
@UltraBob
UltraBob merged commit 345e096 into main Jul 24, 2026
3 checks passed
@UltraBob
UltraBob deleted the fix/node-install-quoting-and-failure-reporting branch July 24, 2026 21:33
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.

Node tooling install silently reports success after a failed npm install (version constraints are shell-escaped)

1 participant