You regenerate after a spec change and the working tree turns red with 240 modified files. Most are reformatting, some are the change you wanted, and one is a configuration file you edited by hand three months ago. Finding out which is which now costs an afternoon, and reverting the wrong one costs a release.
What you get
You will end up with a report you read before anything is written: what would be created, what would be replaced and how, and what the generator’s own rules protect. You also get a dry run that can be run twice with the same answer. This is for you if you regenerate into a project people edit.
Short answer
Run the generator in dry-run mode and read the report before you let it write. It should name every file it would create, every file it would replace with a diff of the change, and every file its mode protects. A dry run that writes nothing can be repeated, which is what lets you compare two runs and see that only the intended files move.
You will need
Node 22 or later, and a generator that writes into a project you have edited. The file modes used here follow what jostraca calls write, preserve, and merge, and most generators offer something equivalent under another name. The idea is borrowed from infrastructure tools, where a plan you read before applying has been the default for years.1
Voxgig maintains sdkgen and jostraca. This page compares their dry run with OpenAPI Generator and with generating into a temporary directory.
Approaches compared
| Approach | When it fits | What it costs you | When to pick something else |
|---|---|---|---|
| A built-in dry run | The generator has one, and it reports modes as well as changes | Trust in the generator’s own report, which you cannot check without running it | The generator has no such mode |
| OpenAPI Generator into a temp directory | Any generator, because it needs nothing from the tool beyond an output path | A second copy of the output, and a comparison you drive yourself | The generator merges into existing files rather than replacing them |
| generate then git diff | You are already in a clean tree and can revert cheaply | The files are written before you look, so a protected file may already be gone | Someone has uncommitted work in the same tree |
| jostraca file modes | You want the protection declared per file rather than remembered per run | Modes to set, and a merge that can still conflict on a hand-edited file | Nobody edits the generated tree |
The real difference is when you find out. Writing first and reading the diff afterwards works perfectly in a clean tree, and it is the common case. It fails on the day somebody has uncommitted work, or the generated tree is not in version control, or a file the generator replaces was never committed in the first place.
A dry run also gives you something a diff after the fact does not: the generator’s own reasoning. A file kept because its mode says preserve is a different fact from a file that happens to be unchanged, and only the tool can tell you which one applies. That distinction is what stops a team from deleting a mode they think is doing nothing.
Report the mode, not only the change
Three outcomes per file, and the third is the one people forget.
if (file.mode === 'preserve') {
report.push({ path: file.path, action: 'keep', why: 'mode is preserve' })
continue
}
if (before === file.content) {
report.push({ path: file.path, action: 'same' })
continue
}
report.push({ path: file.path, action: 'replace', diff: diff(before, file.content, file.path) })
The words keep and same look identical in a working tree and they mean opposite things. A file
that is the same would be overwritten harmlessly on the next run. A file that is kept would be
overwritten destructively, and is not, because somebody set a mode.
Include the diff in the report rather than only a count. Two hundred changed files is a number; two hundred diffs where 195 are a changed generator banner is a decision you can make in a minute.
Make the dry run provably inert
The strongest property of a preview is that running it changes nothing.2
console.log('dry run against the checked-in project')
show(plan('project'))
The same call with apply set is the only path that writes. One code path for both, with writing
behind a flag, is what stops the preview and the real run from drifting apart. A preview that lies
is worse than no preview, because it is believed.
Test that property directly. A test that runs the plan twice and asserts the same report is a test that the preview wrote nothing, and it will fail the day somebody adds a cache file.
Check it worked
Run the preview against a project that carries a hand edit.
node demo.mjs
dry run against the checked-in project
client.js replace
--- client.js
+++ client.js
-export function listMeters(cursor) {
- return request('GET', '/meters', { cursor })
+export function listMeters(cursor, limit) {
+ return request('GET', '/meters', { cursor, limit })
}
+export function loadMeter(id) {
+ return request('GET', `/meters/${id}`)
+}
+
config.js keep (mode is preserve)
README.md create
Three files, three outcomes. The client gains a parameter and an operation, which is the change the spec asked for. The configuration file carries a hand-edited timeout and is kept, with the reason stated. The README does not exist yet, so it is a create rather than a diff nobody needs to read.
node --test generate.test.mjs
1..6
# tests 6
# suites 0
# pass 6
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 122.73695
When it goes wrong
The preview and the real run disagree. Two code paths. Make the writing a flag on the same function, and test that a dry run leaves the tree byte-identical.
Every file shows as replaced. Line endings or a trailing newline differ between the generator and whatever last wrote the files. Normalize before comparing, or the report is noise. The same problem shows up in kubectl diff, and the fix there is the same: compare the values, not the bytes.
A protected file was overwritten anyway. The mode was set on a path that no longer matches, usually after a rename. Fail the run when a mode names a file the plan does not produce.
The diff is unreadable. The generator reorders keys or regenerates a timestamp. Sort deterministic output and keep timestamps out of generated files entirely.
When not to do this
Do not use a dry run instead of version control. A preview tells you what one run would do, and history tells you what every run has done. Commit the generated tree, or accept that you cannot answer questions about last month.
Do not let a preview become the review. Reading 240 diffs on a terminal is not a review, and a generator that produces 240 diffs for a one-field change has a formatting problem worth fixing first.
Do not rely on sdkgen’s dry run to protect a file you have not given a mode. The report tells you what the configured modes would do, and a file nobody protected is a file the next run replaces.
Related how-tos
Last verified
Verified 2026-09-14 against Node 22.22.2. Both output blocks are what the preceding command printed.
Footnotes
-
Terraform reports the outcome of a plan through its exit status if asked. With
-detailed-exitcode, the command exits 0 for an empty diff, 1 for an error, and 2 when there are changes.kubectl diffexits 0 for no differences, 1 for differences, and any larger number for an error, and expects any replacement diff program to follow the same convention. Two tools agree that a change deserves an exit code of its own, and disagree about which one. ↩︎ Back to text -
GNU make’s version of the idea has four names. The manual lists
-n,--just-print,--dry-run, and--reconfor one flag, describes it as a “No-op” and adds that some recipes are executed anyway. A dry run with an exception clause is the usual kind, and this one carries its exception in the same paragraph as its promise. ↩︎ Back to text