How-to › Ship an SDK

How to preview a regeneration as a diff before writing files#

See exactly what a generator would change before it changes anything, with a dry run that reports creates, replacements, and the files its mode protects.

Audience
API producer
Level
intermediate
Topic
Regenerate without losing edits
Languages
TypeScript and JavaScript
Verified

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

ApproachWhen it fitsWhat it costs youWhen to pick something else
A built-in dry runThe generator has one, and it reports modes as well as changesTrust in the generator’s own report, which you cannot check without running itThe generator has no such mode
OpenAPI Generator into a temp directoryAny generator, because it needs nothing from the tool beyond an output pathA second copy of the output, and a comparison you drive yourselfThe generator merges into existing files rather than replacing them
generate then git diffYou are already in a clean tree and can revert cheaplyThe files are written before you look, so a protected file may already be goneSomeone has uncommitted work in the same tree
jostraca file modesYou want the protection declared per file rather than remembered per runModes to set, and a merge that can still conflict on a hand-edited fileNobody 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.

Last verified

Verified 2026-09-14 against Node 22.22.2. Both output blocks are what the preceding command printed.

Footnotes

  1. 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 diff exits 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

  2. GNU make’s version of the idea has four names. The manual lists -n, --just-print, --dry-run, and --recon for 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

Read this page as markdown · All how-to guides

Generate the client instead of writing it#

Retries, timeouts, pagination and auth are the same problems in every client. Voxgig generates them from your OpenAPI description, in 23 languages, from one model.

Get the Voxgig dispatch

Short notes on building SDKs, CLIs, REPLs, and MCPs for API-first teams, plus the occasional Fireside episode pick.

By signing up you agree to our Terms and Conditions.