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
Problem
DomStack's build steps write files directly into the destination directory without one shared check for duplicate output paths.
This can affect:
outputNamePages 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
--copydocumentation 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:
DOM_STACK_ERROR_OUTPUT_CONFLICTPossible 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:
Renames and removed outputs must also release old paths at the correct time.
Cases to define
workers.jsonversus another output--copycollisionsfeedandfeed/index.xmlChanging the documented
--copybehavior may require release notes.Acceptance tests