Skip to content

fix(build): stop --dry-run deleting dist/ and honor build.outDir - #3566

Merged
kojiwakayama merged 2 commits into
mainfrom
fix/dx-20260811-b2-4
Aug 11, 2026
Merged

fix(build): stop --dry-run deleting dist/ and honor build.outDir#3566
kojiwakayama merged 2 commits into
mainfrom
fix/dx-20260811-b2-4

Conversation

@kojiwakayama

@kojiwakayama kojiwakayama commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Found on a DX dogfood walk: a fresh project was scaffolded, veryfront build
was run against it from outside the monorepo, and the output directory was
watched.

Symptom

Two separate ways for veryfront build to destroy a project's dist/ with no
warning.

1. --dry-run deletes the output directory. With three files in dist/
(index.js, nested/deep.txt, a marker file):

$ veryfront build --dry-run
  ! Dry run: no files will be written
  ✓ Built in 0.65s
    1 page, 0 chunks, 0 assets
    4.63 KB in dist
$ find dist
find: dist: No such file or directory

The directory and everything in it is gone, and nothing was written to replace
it. A developer who runs a dry run to preview a build loses their previous
build output.

2. build.outDir is silently ignored. With
export default { build: { outDir: "custom-out" } } in veryfront.config.ts,
the build still cleared and wrote dist/ and reported 4.63 KB in dist;
custom-out/ was never created. No warning, no error. The config file is
loaded — the same file with ssg: false changes the build's behaviour — so
outDir was read into the config object and then dropped. That left -o/--output
as the only working escape hatch, and nothing in the CLI says so.

Root cause

setupBuildDirectories (src/build/production-build/build/build-setup.ts) ran
an unconditional adapter.fs.remove(outputDir, { recursive: true }) before
its if (dryRun) return guard, so a dry run cleaned the output directory and
then skipped everything that would have refilled it.

buildCommand (cli/commands/build/command.ts) hardcoded
options.outputDir ?? join(options.projectDir, "dist"), computed before
getConfig() was even called, so config.build.outDir never reached the build.

Fix

  • Move the clean step behind the dry-run guard. A real build still clears its
    output directory, so stale artifacts from a previous build cannot leak into a
    new one.
  • Resolve the output directory from the loaded config:
    -o/--output > build.outDir > dist. A relative outDir resolves against
    the project directory. displayBuildConfig moved after getConfig() so the
    displayed, reported and written directory are the same one.
  • build.outDir moves from "accepted for compatibility, no built-in semantics"
    to "core runtime/build" in src/config/README.md, and docs/guides/deploying.md
    no longer tells readers the builder ignores it.

Regression tests

Test Why there
src/build/production-build/build/build-setup.test.ts — "leaves an existing output directory untouched in dry run" / "still clears the output directory for a real build" Unit-level, next to the guard that was in the wrong order. The pre-existing tests in this file use a mock adapter whose remove is a no-op, which is precisely why they could not see the deletion; these use an adapter that deletes for real.
cli/commands/build/command.test.tsresolveBuildOutputDir cases Pure resolution rules (flag beats config, config beats default, absolute stays absolute), alongside the existing formatBuildOutputPath tests.
tests/integration/server/build/build.test.ts — new "output directory" block End-to-end through the real config loader and the real build. The build.outDir case drives buildCommand itself, since that is where the directory is decided, and asserts on what the CLI prints (… in custom-out) — the exact line that read … in dist before. Added inside the existing suite rather than a new file so no new sanitizer opt-out is introduced (lint:sanitizer-baseline stays at 404/404).

Both integration tests were confirmed red before the fix: the dry-run one fails
with NotFound … /dist/index.js, the outDir one fails with the captured CLI
output showing 4.63 KB in dist.

Also verified against the real CLI on a scratch project outside the repo:
--dry-run now writes and deletes nothing; build.outDir: "custom-out" writes
to custom-out/ and leaves dist/ intact; -o flagout still overrides the
config; with no config the default is still dist and stale artifacts are still
cleared.

Review follow-up

Honoring build.outDir made a previously inert config value load-bearing, and
review caught the one new hazard that created: since the build clears its output
directory before writing, an outDir of . or .. would have recursively
deleted the project's own source, or the workspace above it. A compatibility-era
config that was harmless while the field was ignored would have become
destructive.

resolveBuildOutputDir now rejects any output directory that is the project
directory or contains it, and throws rather than quietly falling back to dist
silently substituting a different directory is the ignored-configuration bug this
PR exists to fix. The guard covers -o/--output as well as the config path;
veryfront build -o . could already delete the project before this PR, and a
guard on only one of the two inputs to the same function is a half-guard. A
sibling directory such as ../shared-dist is unaffected.

Also added the missing import { defineConfig } from "veryfront"; to the
deploying.md snippet, matching every other config example under docs/guides/.
On a PR whose whole premise is following the published docs literally, a snippet
that does not compile when pasted is in scope.

Declined on the record: --preset embedded still ignores build.outDir. It also
ignores --dry-run (it writes files anyway), --no-split, --no-compress,
--prefetch, --ssg/--no-ssg, --include and --exclude, and never calls
getConfig() at all — handleBuildCommand branches to handleEmbeddedBuild
before the config is loaded and passes it exactly one option. That is a
pre-existing, uniform condition this PR neither created nor worsened, and fixing
outDir alone would leave the same silent-ignore for six other flags. Filed as
#3585.

Not fixed here

A build that fails still leaves the developer with neither the old artifact
nor a new one, because the clean happens up front. Fixing that properly means
staging the build and swapping it in at the end — createBuildPublication in
src/build/production-build/build/build-publication.ts already does exactly
this for the asset pipeline and is the obvious vehicle — but routing the whole
production build through it is a much larger change than this one and belongs
in its own PR.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@kojiwakayama, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 27 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: cb3cf01e-6702-40be-ad5e-1c5395e2a42d

📥 Commits

Reviewing files that changed from the base of the PR and between 718355c and e3b8c68.

📒 Files selected for processing (8)
  • cli/commands/build/command-help.ts
  • cli/commands/build/command.test.ts
  • cli/commands/build/command.ts
  • docs/guides/deploying.md
  • src/build/production-build/build/build-setup.test.ts
  • src/build/production-build/build/build-setup.ts
  • src/config/README.md
  • tests/integration/server/build/build.test.ts

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 830a6aa4e1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cli/commands/build/command.ts
Comment thread cli/commands/build/command-help.ts
Comment thread docs/guides/deploying.md
A dry run cleared the output directory before the dry-run guard, so
`veryfront build --dry-run` printed "Dry run: no files will be written"
and then deleted the whole of dist/ without writing anything back.

`build.outDir` was parsed into the config and then dropped: the CLI
hardcoded `dist`, so the documented way to point the build away from a
project's own dist/ silently did nothing.

Resolve the output directory from the loaded config in buildCommand
(`-o/--output` > `build.outDir` > `dist`, relative paths against the
project dir) and move the clean step behind the dry-run guard.
Honoring build.outDir made a previously inert config value load-bearing:
setupBuildDirectories clears the output directory before writing, so
`outDir: "."` or `".."` would have recursively deleted the project's
own source or the workspace above it.

resolveBuildOutputDir now rejects any output directory that is the
project directory or contains it, for -o/--output as well as the config
path (`veryfront build -o .` could already do this). It throws rather
than falling back to dist, because silently substituting a different
directory is the ignored-configuration bug this change exists to fix.

Also add the missing defineConfig import to the deploying.md snippet so
it compiles when pasted, matching every other config example under
docs/guides/.
@kojiwakayama
kojiwakayama added this pull request to the merge queue Aug 11, 2026
Merged via the queue into main with commit 1522412 Aug 11, 2026
33 checks passed
@kojiwakayama
kojiwakayama deleted the fix/dx-20260811-b2-4 branch August 11, 2026 11:01
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.

1 participant