Back to blog
engineering·5 min read·1 view

The Second Run

Jostraca promises to keep your hand edits when it regenerates files. I tested that promise against version 0.39.0, found three ways it loses data without saying so, and found out why its author's own SDK generator turns the merge off.


Yesterday a code generator deleted part of my README. I had used Voxgig's sdkgen to generate an SDK for an API I run, added a short "Try it" section to the README by hand, then changed one setting and regenerated. The section was gone. Nothing warned me, and there was no backup.

The library that wrote the file is Jostraca, a small MIT-licensed code generator with a TypeScript and a Go implementation. What made this strange is that Jostraca's main selling point is the exact situation I was in. Its tutorial opens with the claim that "a generator earns its keep on the second run," after somebody has edited the output. So I installed it (0.39.0, Node 24) and tested that claim directly.

You describe a file tree in ordinary JavaScript, and Jostraca writes it:

import { Jostraca, Project, File, Content } from 'jostraca'
 
const j = Jostraca({ existing: { txt: { write: true, merge: true } } })
 
const run = (body) => j.generate({ folder: './out' }, () => {
  Project({}, () => File({ name: 'config.sh' }, () => Content(body)))
})
 
await run('PORT=8080\nHOST=localhost\n')
// a person appends DEBUG=1 to out/config.sh
await run('PORT=9090\nHOST=localhost\n')

With merge: true, the second run keeps both changes: PORT=9090 from the generator, DEBUG=1 from the person. Jostraca stores a copy of each generated file under .jostraca/generated/ and uses it as the common ancestor for a three-way merge, the same idea git uses for branches. When both sides edit the same line, you get familiar conflict markers.

This works, and few generators attempt it. Yeoman asks you file by file whether to overwrite. Projen marks its output read-only and tells you to edit the config instead. Copier merges when you update a project from its template, but it does that through git, one project at a time. Jostraca puts a three-way merge behind one option in a library call, which is a good idea.

The trouble is at the edges. First, the merge needs an ancestor, and when there isn't one it overwrites. Suppose you write admin.js by hand, and a later version of your generator starts producing a file with the same name:

writeFileSync('./out/admin.js', 'export const secret = 42\n')
 
const r = await j.generate({ folder: './out' }, () => Project({}, () => {
  File({ name: 'admin.js' }, () => Content('// gen admin\n'))
}))
 
r.files.written    // [ 'out/admin.js' ]  and your code is gone

merge was on. The result reports the file as written, the same as any new file.

Second, a conflict doesn't fail anything. I edited PORT by hand and changed it in the generator too. generate() resolved normally and logged nothing, and config.sh was left like this:

<<<<<<< GENERATED: 2026-10-08T12:23:42.590Z/merge
PORT=1111
=======
PORT=7000
>>>>>>> EXISTING: 2026-10-08T12:23:42.588Z/merge
HOST=localhost

The only sign is r.files.conflicted, which your build script has to remember to check. Git at least stops and tells you a merge is in progress. Here a CI job that regenerates code can go green with a broken shell script in the tree. When I ran it a third time without resolving, the generator's new value was skipped, again without a warning.

Third, protection is a substring match. Any existing file that contains the text JOSTRACA_PROTECT anywhere is never overwritten. That includes a file your own generator wrote, if the generated text happens to mention the feature:

const guide = (v) => Jostraca().generate({ folder: './out' }, () =>
  Project({}, () => File({ name: 'GUIDE.md' }, () =>
    Content(`# Guide v${v}\nAdd JOSTRACA_PROTECT to a file to keep your edits.\n`))))
 
await guide(1)
await guide(2)   // GUIDE.md still says v1, and every result array is empty

The guide froze at version 1, and nothing in the result says why.

To be fair, the docs describe all of this. Jostraca's reference page is unusually candid. It has a section listing options that validate and then have no effect, a heading admitting that merge falls back to overwriting without a word when it has no baseline, and a table of options whose global setting is ignored. I liked reading it. But documenting a data-loss path is a weaker fix than closing it. Taking a .old backup when a merge has no ancestor, or throwing on conflict by default, would each be a small change.

The candour has a second problem: the docs don't match the release. The options table says a global build: false is ignored. In 0.36.6, the version the homepage install command pins, that's true. In 0.39.0, the current release, it isn't. Jostraca shipped 22 versions between May and September, seven of them in three days, so prose like this falls out of date quickly. The docs say their code examples run in the test suite. That guarantee doesn't cover the tables.

None of this explains my README, though. The reason was simpler. This is how sdkgen configures Jostraca:

existing: {
  txt: {
    write: true,
    merge: false
  }
}

The tool that brought me to Jostraca, written by the same author, turns the merge off.

My first reaction was that this was a bug. I now think sdkgen is right. My SDK contained about 770 files, and Voxgig's SDK catalogue has about 800 repositories. At that scale a text merge would sooner or later put conflict markers into files nobody reads, and as shown above, nothing will stop the build. So sdkgen does something else. It overwrites generated files every time, and it keeps your decisions in an overlay file, model/project.aontu, which the scaffold creates once and never touches again. Generated files are disposable, and hand edits go somewhere the generator never writes. Projen and the protobuf toolchains work the same way.

That leaves Jostraca's merge in an awkward spot. It's careful work aimed at a problem that its own most serious user decided to design away. It does have a place: small generators whose output a person adopts and maintains afterwards, like the tutorial's config.sh, or a scaffold you run a few times early in a project. For anything that regenerates continuously, I would follow sdkgen and keep merge off.

If I maintained Jostraca, I would start with the homepage and make it describe what its authors do in practice: overwrite, plus a place for hand edits. Then I would make the merge fail loudly, backing up any file it can't merge and throwing on conflict, and replace the substring check with a marker comment that generated text can't trigger by accident.

As for my README, the section belongs in the overlay. sdkgen has a text slot for extra README content in project.aontu, and when I moved a test section there it came through regeneration intact. I found that by reading sdkgen's source. I would rather have learned it from Jostraca's homepage.


Enjoyed this?

Be the first to react

ShareXLinkedIn