Skip to content

Align prettier wrappers with project-root CWD and drop path rewriting - #56

Draft
UltraBob wants to merge 2 commits into
mainfrom
issue-46-prettier-root-cwd
Draft

Align prettier wrappers with project-root CWD and drop path rewriting#56
UltraBob wants to merge 2 commits into
mainfrom
issue-46-prettier-root-cwd

Conversation

@UltraBob

@UltraBob UltraBob commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Closes #46.

What changed

commands/web/prettier was the only wrapper in the suite that ran from the Drupal docroot. To compensate, the pair rewrote user-supplied relative paths in opposite directions — the check wrapper stripped the web/ prefix, the fix wrapper added it. A relative path therefore meant something different to ddev prettier than to ddev phpcs, and ddev prettier accepted a path shape stock prettier rejects.

Both wrappers now run from the project root, pass relative paths through untouched, translate host-absolute paths via map_path only, and inject the recorded docroot as the default target on bare runs — the same shape as the stylelint pair (precedent: #32).

The "check this first" question in the issue is answered: the docroot CWD was not load-bearing. Config and ignore files are passed as absolute paths, and Prettier resolves ignore patterns relative to the ignore file's own directory, so the CWD only affected relative-argument resolution and the bare-run target.

Behavior changes worth a release note

  • -c is no longer treated as --config. In prettier's CLI -c is short for --check; it now passes through. Use --config to point at a config file. The old grouping silently swallowed the next argument.
  • prettier-fix now scopes by the project .prettierignore alone, matching the check wrapper, instead of prettier's default .gitignore + .prettierignore set. Files that are gitignored but not prettierignored are now formatted by fix — previously check flagged them and fix refused to touch them.
  • File targets are passed after --, so a filename beginning with a dash is treated as a path rather than an unknown option.
  • --stdin-filepath now suppresses the default docroot target, so piped stdin is actually checked instead of silently ignored while the whole docroot got scanned.
  • ddev prettier --preview now errors with a pointer to prettier-fix instead of exiting 0 without checking anything.

Preview mode was rebuilt

prettier-fix --preview previously copied the project tree, formatted the copy, and diffed the copy against the real tree. Every way the copy differed from the real project — excluded directories, config and ignore files resolved against the wrong root, concurrent writes — produced a preview that promised something other than what apply did. Successive review rounds kept finding new instances of that same class.

It now asks prettier which files the apply run would rewrite, using the identical arguments that run will use, then diffs each file against prettier's own formatted output. There is no copy, so there is nothing to diverge:

  • Custom --config / --ignore-path values and directory-anchored ignore patterns behave identically in preview and apply.
  • Files in directories the old copy excluded (.trunk, dcq-reports, nested vendor) are previewed correctly.
  • The patch uses project-relative a/ b/ headers, quoted the way git quotes names containing whitespace, so it applies with patch -p1.
  • --preview is rejected alongside --stdin-filepath and explicit output-mode flags, which would have made the listing run write files.
  • The patch is truncated before work starts, so a failed run can't leave an earlier patch to be mistaken for the current one.

Testing

Verified with real ddev commands against sandboxes/runway-ops (fresh add-on install with root Node toolchain) and ux-test-d11: bare runs, root-relative / docroot-relative / host-absolute paths, .inc exclusion, --help, --version, --, both --config forms, host shim invocation, ddev checks integration, preview→apply parity with a custom ignore file, filenames containing spaces, leading-dash filenames, a hostile .dcq-docroot, and the --preview flag-conflict guards. bash -n passes on both wrappers.

Docs

README's fix-commands paragraph and dcq-reports/ note corrected — stylelint-fix dropped --preview in its refactor, and the preview flow reports a change count and patch path rather than displaying the patch.

Follow-ups not in this PR

  • The eslint pair still has the copy-based preview architecture this PR replaced, and ESLint may not share prettier's refusal to follow symlinks — worth its own audit.
  • ddev eslint --preview has no friendly rejection.
  • .ddev/** is not in the shipped .prettierignore, so whole-root scans trip over the add-on's own #ddev-generated assets.

kalabot added 2 commits August 9, 2026 00:43
The prettier check wrapper was the only command in the suite that ran from
the Drupal docroot, and the pair compensated by rewriting user-supplied
relative paths in opposite directions (check stripped the docroot prefix,
fix added it). A relative path therefore meant something different to
ddev prettier than to every other wrapper, and ddev prettier accepted a
path shape stock prettier would reject.

Both wrappers now run from the project root, pass user-supplied relative
paths through untouched, translate host-absolute paths via map_path only,
and inject the recorded docroot as the default target on bare runs - the
same shape as the stylelint pair. The docroot CWD was not needed for CI
parity: config and ignore files are passed as absolute paths, and Prettier
resolves ignore patterns relative to the ignore file's location.

Arguments are now split into options and file targets, and the targets are
passed after --, so a filename that begins with a dash is treated as a path
rather than an unknown option.

Behavior changes beyond the CWD alignment:

- -c is no longer treated as --config. In prettier's CLI, -c is short for
  --check; it now passes through. Use --config to point at a config file.
- prettier-fix now scopes by the project .prettierignore alone (matching
  the check wrapper) instead of prettier's default .gitignore +
  .prettierignore set, so check and fix agree on the file set. Files that
  are gitignored but not prettierignored are now formatted by fix, since
  check already flagged them.
- --stdin-filepath suppresses the default docroot target so piped stdin is
  actually checked, and its value is host-path mapped.
- Both wrappers resolve prettier/bin/prettier.cjs and invoke it via node,
  like the rest of the suite, instead of the check wrapper depending on
  the .bin shim's executable bit.
- The injected default docroot is validated to stay inside the project
  root, and a value-taking flag left dangling at the end of the arguments
  no longer swallows the injected target.
- ddev prettier rejects --preview with a pointer to prettier-fix instead
  of exiting 0 without checking anything.

Preview mode is rebuilt so it cannot disagree with the apply it precedes.
It previously copied the project tree, formatted the copy, and diffed the
copy against the real tree. Every difference between the copy and the real
project - excluded directories, config and ignore files resolved against
the wrong root, concurrent writes - showed up as a preview that promised
something other than what apply did.

It now asks prettier which files the apply run would rewrite, using the
identical arguments that run will use, then diffs each of those files
against prettier's own formatted output. There is no copy to diverge from
the project, so the patch is exactly the pending edit:

- Custom --config and --ignore-path values, and ignore patterns anchored to
  a directory, behave the same in the preview as in the apply.
- Files in directories the old copy excluded are previewed correctly.
- The patch is written with project-relative a/ b/ headers, quoted the way
  git quotes names that contain whitespace, so it applies with patch -p1.
- --preview is rejected alongside --stdin-filepath and explicit output-mode
  flags, which would have made the listing run write files.
- The patch is truncated before work starts, so a failed run cannot leave
  an earlier run's patch behind to be mistaken for the current one.
- Report directory, temp directory, and diff failures abort cleanly.
- A file literally named --preview can be targeted after --.
stylelint-fix dropped --preview in the wrapper refactor and points users
at git diff instead, and the preview flow reports a change count and
patch location rather than displaying the patch. Update the fix-commands
paragraph and the dcq-reports note to match.
@UltraBob

UltraBob commented Aug 9, 2026

Copy link
Copy Markdown
Owner Author

Needs a deep review before merging

Flagging this explicitly: do not merge on the strength of the green checks. This branch went through four full-scope review rounds, and every round found real defects — including defects inside the previous round's fixes. That pattern is the reason it is a draft.

What each round surfaced, in order:

  1. Clean.
  2. Preview wrote to the real tree for out-of-root targets; a trailing-slash root argument made preview report "no changes" while apply would reformat the whole tree; glob targets produced empty patches.
  3. The fix from round 2 was itself wrong — the wrapper-injected absolute --ignore-path silently disabled directory-anchored ignore patterns inside the preview copy. Also: patch headers pointing at deleted temp directories, an unchecked mktemp that could have sent cp -a to /, and a hostile .dcq-docroot escaping the project root.
  4. The round-3 fixes were still not airtight: user-supplied absolute --config / --ignore-path anchored to the real tree while formatting happened in the copy (preview and apply disagreed on both the file set and the content), GNU diff quoted headers for filenames with spaces defeated the header rewrite, and the tar excludes hid changes that apply would make.

Rather than patch round 4's findings individually, the preview was rebuilt to remove the failure class: no tree copy, so nothing can diverge from the real project. That redesign is newer than any review round, so it has had no adversarial review at all — only the direct verification described in the PR body. It is the part that most needs eyes.

Specific things worth a reviewer's attention:

  • The --list-different → per-file stdout diff approach costs one prettier invocation per changed file. Correct, but slower than a single pass on a project with many unformatted files. Is that trade acceptable?
  • prettier-fix no longer honours .gitignore. This is deliberate (check and fix now agree), but it means fix will reformat gitignored build artifacts that were previously left alone. That is a real behavior change for existing users.
  • -c no longer means --config. Anyone relying on the old, incorrect grouping gets a silent semantic change.
  • Filenames containing newlines will break the line-based --list-different parsing. Judged acceptable; flagging it as a known limitation rather than an oversight.

The eslint pair still uses the copy-based preview architecture that was replaced here, so the round 2–4 findings are worth re-checking against it as separate issues.

@UltraBob

UltraBob commented Aug 9, 2026

Copy link
Copy Markdown
Owner Author

Test status, stated precisely

The bats suite for these commits passed in CI (tests (stable), 23 min). tests (HEAD) was still running at the time of writing.

Local runs need a caveat. The full local suite passed twice against earlier revisions of this branch, and the interactive --preview apply path is covered by the fix commands preview mode test that passed in those runs. Against the final revision — the rebuilt preview — the local suite did not complete: two bats runs overlapped and wedged the local Docker daemon, which is a known local-environment race, not a signal about the code. CI's pass is the meaningful result for this revision.

Everything else in the PR body was checked by hand with real ddev commands against sandboxes/runway-ops after the redesign, including the preview↔apply parity cases that motivated it.

@UltraBob

UltraBob commented Aug 9, 2026

Copy link
Copy Markdown
Owner Author

Local test caveat withdrawn — both suites now pass against this revision

The local Docker daemon recovered, so I re-ran the suites serially against the final commits:

  • bats tests/test.bats --filter-tags '!release'52/52 pass, exit 0
  • DCQ_FULL_TESTS=1 bats tests/test.bats --filter-tags full2/2 pass, exit 0, including fix commands preview mode, which drives the interactive --preview → confirm → apply path against the rebuilt preview

That closes the gap described in my previous comment: the earlier failure to complete was the local parallel-run race, not the code. Together with tests (stable) and tests (HEAD) green in CI, the full suite has now passed four times against this revision on three environments.

The request for deep review stands unchanged — the preview redesign is still newer than every adversarial review round, and passing tests is not the same as having been reviewed.

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.

Align prettier wrappers with project-root CWD and drop path rewriting

1 participant