Skip to content

Detect conflicting output paths across build steps #288

Description

@bcomnes

Problem

DomStack's build steps write files directly into the destination directory without one shared check for duplicate output paths.

This can affect:

  • a regular or generated page and a template that target the same file
  • two templates that return the same outputName
  • pages or templates that overlap esbuild, static, or copied files
  • two copy sources that contain the same destination path

Pages and templates are rendered concurrently. If they target the same path, both writes can succeed and the final contents depend on which write finishes last. Other build steps run separately but can still replace files written by another step.

Manifest reconciliation does not prevent this. It is optional, runs after files have been written, and only warns when output records disagree. The --copy documentation currently says conflicting files are not detected and have undefined behavior.

PR #253 adds a deliberately narrower check: generated pages cannot replace regular pages or other generated pages. That matches its generated-pages design and does not attempt to solve this existing whole-build problem.

Desired behavior

A successful build should have no output path written by more than one source unless that sharing is explicitly supported.

When two sources target the same path, DomStack should:

  • fail with DOM_STACK_ERROR_OUTPUT_CONFLICT
  • include the destination-relative output path
  • identify both source files or build steps
  • avoid silently choosing one file's contents
  • behave consistently in one-shot and watch builds
  • keep watch state bounded to current outputs

Possible approaches

1. Shared output-path registry

Create one registry for a build and pass it to every writer. A build step registers a destination path before writing it. Registering a path already owned by another source throws an output-conflict error.

This fits pages and templates well, including template arrays and async iterators, because each dynamic output can register its path immediately before its first write. Paths known before rendering, such as page HTML, can be registered up front so a template never overwrites them.

The harder part is esbuild and copy tools that currently write directly to the final destination. They may need a planning hook, an output callback, or temporary output directories.

2. Build-step staging directories

Have each build step write into its own temporary directory, collect all output records, reject duplicate paths, and only then move successful outputs into the destination.

This gives the strongest protection and avoids partially replacing a conflicting destination file, but it adds disk I/O and requires a clear merge and cleanup design.

3. Hybrid approach

Use a shared registry for outputs DomStack writes directly and staging for tools that control their own writes. A final duplicate-record check can remain as a safety net, but it should not be the main protection because records are normally available only after writing.

Watch-mode considerations

Filtered watch builds return only the outputs rebuilt for one change. Conflict checking therefore cannot treat that partial list as the complete site.

A possible model is:

  • remember output path → source from the latest successful full build
  • allow a source to replace its own previous outputs during a filtered rebuild
  • reject attempts to take a path owned by another source
  • update ownership after a successful filtered rebuild
  • replace the complete map after each successful full rebuild so it cannot grow without bound
  • leave ownership unchanged after failed builds

Renames and removed outputs must also release old paths at the correct time.

Cases to define

  • page HTML versus template output
  • template versus template, including arrays and async iterators
  • page workers.json versus another output
  • esbuild, service worker, static, and --copy collisions
  • the generated DomStack manifest versus user outputs
  • exact duplicate records from the same source
  • file-versus-directory conflicts such as feed and feed/index.xml
  • normalized path separators and case-insensitive filesystems
  • failed builds that have already written some outputs

Changing the documented --copy behavior may require release notes.

Acceptance tests

  • regular and generated page/template conflicts fail with both sources in the error
  • template/template conflicts fail for single objects, arrays, and async iterators
  • conflicts with esbuild, static, and copied outputs fail
  • no conflicting writer silently determines final contents
  • filtered watch builds detect conflicts with outputs from the latest successful full build
  • watch output tracking remains bounded and recovers after renames, removals, and failed builds
  • non-conflicting one-shot and watch builds retain their current behavior

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions